Initial migration of Cloud Shell feature

This commit is contained in:
alexweininger 2024-04-24 16:33:06 -07:00
Родитель 152cbe37d5
Коммит e468390050
17 изменённых файлов: 3125 добавлений и 489 удалений

2136
package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -2,7 +2,7 @@
"name": "vscode-azureresourcegroups",
"displayName": "Azure Resources",
"description": "%azureResourceGroups.description%",
"version": "0.8.5",
"version": "0.8.6",
"publisher": "ms-azuretools",
"icon": "resources/resourceGroup.png",
"aiKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255",
@ -30,11 +30,26 @@
"preview": true,
"activationEvents": [
"onFileSystem:azureResourceGroups",
"onWalkthrough:azure-get-started"
"onWalkthrough:azure-get-started",
"onTerminalProfile:azureResources.cloudShellBash"
],
"main": "./main.js",
"browser": "./dist/web/extension.bundle.js",
"contributes": {
"terminal": {
"profiles": [
{
"title": "Azure Cloud Shell (bash)",
"id": "azureResources.cloudShellBash",
"icon": "$(azure)"
},
{
"title": "Azure Cloud Shell (pwsh)",
"id": "azureResources.cloudShellPowerShell",
"icon": "$(azure)"
}
]
},
"x-azResources": {
"commands": [
{
@ -603,13 +618,16 @@
"@types/gulp": "^4.0.6",
"@types/mocha": "^7.0.2",
"@types/node": "18.19.x",
"@types/request-promise": "^4.1.51",
"@types/semver": "^7.3.12",
"@types/uuid": "^9.0.1",
"@types/vscode": "^1.81.0",
"@types/ws": "^8.5.10",
"@typescript-eslint/eslint-plugin": "^5.59.11",
"@vscode/test-electron": "^2.3.8",
"@vscode/vsce": "^2.19.0",
"chokidar": "^3.6.0",
"copy-webpack-plugin": "^12.0.2",
"eslint": "^8.42.0",
"eslint-plugin-import": "^2.27.5",
"glob": "^7.1.6",
@ -626,13 +644,17 @@
"dependencies": {
"@azure/arm-resources": "^5.2.0",
"@azure/arm-resources-profile-2020-09-01-hybrid": "^2.1.0",
"@azure/ms-rest-js": "^2.7.0",
"@microsoft/vscode-azext-azureauth": "^2.3.0",
"@microsoft/vscode-azext-azureutils": "^2.0.0",
"@microsoft/vscode-azext-utils": "^2.4.0",
"buffer": "^6.0.3",
"jsonc-parser": "^2.2.1",
"node-fetch": "^3.3.2",
"request-promise": "^4.2.6",
"uuid": "^9.0.0",
"vscode-nls": "^5.0.1",
"vscode-uri": "^3.0.7"
"vscode-uri": "^3.0.7",
"ws": "^8.16.0"
}
}

Просмотреть файл

@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ReadStream } from "node:fs";
import { CancellationToken, Event, Progress, Terminal, TerminalProfile } from "vscode";
export interface UploadOptions {
contentLength?: number;
progress?: Progress<{ message?: string; increment?: number }>;
token?: CancellationToken;
}
export interface CloudShell {
readonly status: CloudShellStatus;
readonly onStatusChanged: Event<CloudShellStatus>;
readonly waitForConnection: () => Promise<boolean>;
readonly terminal: Promise<Terminal>;
// readonly session: Promise<AzureSession>;
readonly uploadFile: (filename: string, stream: ReadStream, options?: UploadOptions) => Promise<void>;
}
export type CloudShellStatus = 'Connecting' | 'Connected' | 'Disconnected';
export interface CloudShellInternal extends Omit<CloudShell, 'terminal'> {
status: CloudShellStatus;
terminal?: Promise<Terminal>;
terminalProfile?: Promise<TerminalProfile>;
}

Просмотреть файл

@ -0,0 +1,729 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AzureSubscriptionProvider, getConfiguredAzureEnv } from '@microsoft/vscode-azext-azureauth';
import { IActionContext, IParsedError, callWithTelemetryAndErrorHandlingSync, parseError } from '@microsoft/vscode-azext-utils';
import * as cp from 'child_process';
import * as FormData from 'form-data';
import { ReadStream } from 'fs';
import * as http from 'http';
import { ClientRequest } from 'http';
import { Socket } from 'net';
import * as path from 'path';
import * as request from 'request-promise';
import * as semver from 'semver';
import { UrlWithStringQuery, parse } from 'url';
import { CancellationToken, EventEmitter, MessageItem, Terminal, TerminalOptions, TerminalProfile, ThemeIcon, Uri, authentication, commands, env, window, workspace } from 'vscode';
import { ext } from '../extensionVariables';
import { localize } from '../utils/localize';
import { CloudShell, CloudShellInternal, CloudShellStatus, UploadOptions } from './CloudShellInternal';
import { Deferred, delay, logAttemptingToReachUrlMessage } from './cloudConsoleUtils';
import { Queue, Server, createServer } from './ipc';
import { readJSON } from './ipcUtils';
import { HttpLogger } from './logging/HttpLogger';
import { RequestNormalizer } from './logging/request/RequestNormalizer';
function ensureEndingSlash(value: string): string {
return value.endsWith('/') ? value : `${value}/`;
}
function getArmEndpoint(): string {
return ensureEndingSlash(getConfiguredAzureEnv().resourceManagerEndpointUrl);
}
interface OS {
id: 'linux' | 'windows';
shellName: string;
otherOS: OS;
}
export type OSName = 'Linux' | 'Windows';
type OSes = { Linux: OS, Windows: OS };
export const OSes: OSes = {
Linux: {
id: 'linux',
shellName: localize('azure-account.bash', "Bash"),
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
get otherOS(): OS { return OSes.Windows; },
},
Windows: {
id: 'windows',
shellName: localize('azure-account.powershell', "PowerShell"),
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
get otherOS(): OS { return OSes.Linux; },
}
};
async function waitForConnection(this: CloudShell): Promise<boolean> {
const handleStatus = () => {
switch (this.status) {
case 'Connecting':
return new Promise<boolean>(resolve => {
const subs = this.onStatusChanged(() => {
subs.dispose();
resolve(handleStatus());
});
});
case 'Connected':
return true;
case 'Disconnected':
return false;
default:
const status: never = this.status;
throw new Error(`Unexpected status '${status}'`);
}
};
return handleStatus();
}
function getUploadFile(tokens: Promise<AccessTokens>, uris: Promise<ConsoleUris>): (this: CloudShell, filename: string, stream: ReadStream, options?: UploadOptions) => Promise<void> {
return async function (this: CloudShell, filename: string, stream: ReadStream, options: UploadOptions = {}) {
if (options.progress) {
options.progress.report({ message: localize('azure-account.connectingForUpload', "Connecting to upload '{0}'...", filename) });
}
const accessTokens: AccessTokens = await tokens;
const { terminalUri } = await uris;
if (options.token && options.token.isCancellationRequested) {
throw 'canceled';
}
return new Promise<void>((resolve, reject) => {
const form = new FormData();
form.append('uploading-file', stream, {
filename,
knownLength: options.contentLength
});
const uploadUri: string = `${terminalUri}/upload`;
logAttemptingToReachUrlMessage(uploadUri);
const uri: UrlWithStringQuery = parse(uploadUri);
const req: ClientRequest = form.submit(
{
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
protocol: <any>uri.protocol,
hostname: uri.hostname,
port: uri.port,
path: uri.path,
headers: {
'Authorization': `Bearer ${accessTokens.resource}`
},
},
(err, res) => {
if (err) {
reject(err);
} if (res && res.statusCode && (res.statusCode < 200 || res.statusCode > 299)) {
reject(`${res.statusMessage} (${res.statusCode})`)
} else {
resolve();
}
if (res) {
res.resume(); // Consume response.
}
}
);
if (options.token) {
options.token.onCancellationRequested(() => {
reject('canceled');
req.abort();
});
}
if (options.progress) {
req.on('socket', (socket: Socket) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
options.progress!.report({
message: localize('azure-account.uploading', "Uploading '{0}'...", filename),
increment: 0
});
let previous: number = 0;
socket.on('drain', () => {
const total: number = req.getHeader('Content-Length') as number;
if (total) {
const worked: number = Math.min(Math.round(100 * socket.bytesWritten / total), 100);
const increment: number = worked - previous;
if (increment) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
options.progress!.report({
message: localize('azure-account.uploading', "Uploading '{0}'...", filename),
increment
});
}
previous = worked;
}
});
});
}
});
}
}
export const shells: CloudShellInternal[] = [];
export function createCloudConsole(_authProvider: AzureSubscriptionProvider, osName: OSName, terminalProfileToken?: CancellationToken): CloudShellInternal {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return (callWithTelemetryAndErrorHandlingSync('azure-account.createCloudConsole', (context: IActionContext) => {
const os: OS = OSes[osName];
context.telemetry.properties.cloudShellType = os.shellName;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let liveServerQueue: Queue<any> | undefined;
const event: EventEmitter<CloudShellStatus> = new EventEmitter<CloudShellStatus>();
let deferredTerminal: Deferred<Terminal>;
let deferredTerminalProfile: Deferred<TerminalProfile>;
// let deferredSession: Deferred<AzureSession>;
let deferredTokens: Deferred<AccessTokens>;
const tokensPromise: Promise<AccessTokens> = new Promise<AccessTokens>((resolve, reject) => deferredTokens = { resolve, reject });
let deferredUris: Deferred<ConsoleUris>;
const urisPromise: Promise<ConsoleUris> = new Promise<ConsoleUris>((resolve, reject) => deferredUris = { resolve, reject });
let deferredInitialSize: Deferred<Size>;
const initialSizePromise: Promise<Size> = new Promise<Size>((resolve, reject) => deferredInitialSize = { resolve, reject });
const state: CloudShellInternal = {
status: 'Connecting',
onStatusChanged: event.event,
waitForConnection,
terminal: new Promise<Terminal>((resolve, reject) => deferredTerminal = { resolve, reject }),
terminalProfile: new Promise<TerminalProfile>((resolve, reject) => deferredTerminalProfile = { resolve, reject }),
// session: new Promise<AzureSession>((resolve, reject) => deferredSession = { resolve, reject }),
uploadFile: getUploadFile(tokensPromise, urisPromise)
};
// eslint-disable-next-line @typescript-eslint/no-empty-function
state.terminal?.catch(() => { }); // ignore
// eslint-disable-next-line @typescript-eslint/no-empty-function
// state.session.catch(() => { }); // ignore
shells.push(state);
function updateStatus(status: CloudShellStatus) {
state.status = status;
event.fire(state.status);
if (status === 'Disconnected') {
deferredTerminal.reject(status);
deferredTerminalProfile.reject(status);
// deferredSession.reject(status);
deferredTokens.reject(status);
deferredUris.reject(status);
shells.splice(shells.indexOf(state), 1);
void commands.executeCommand('setContext', 'openCloudConsoleCount', `${shells.length}`);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(async function (): Promise<any> {
if (!workspace.isTrusted) {
updateStatus('Disconnected');
return requiresWorkspaceTrust(context);
}
void commands.executeCommand('setContext', 'openCloudConsoleCount', `${shells.length}`);
const isWindows: boolean = process.platform === 'win32';
if (isWindows) {
// See below
try {
const { stdout } = await exec('node.exe --version');
const version: string | boolean = stdout[0] === 'v' && stdout.substr(1).trim();
if (version && semver.valid(version) && !semver.gte(version, '6.0.0')) {
updateStatus('Disconnected');
return requiresNode(context);
}
} catch (err) {
updateStatus('Disconnected');
return requiresNode(context);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const serverQueue: Queue<any> = new Queue<any>();
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const server: Server = await createServer('vscode-cloud-console', async (req, res) => {
let dequeue: boolean = false;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
for (const message of await readJSON<any>(req)) {
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
if (message.type === 'poll') {
dequeue = true;
} else if (message.type === 'log') {
Array.isArray(message.args) && ext.outputChannel.appendLog((<string[]>message.args).join(' '));
} else if (message.type === 'size') {
deferredInitialSize.resolve(message.size as Size);
} else if (message.type === 'status') {
updateStatus(message.status as CloudShellStatus);
}
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
}
let response = [];
if (dequeue) {
try {
response = await serverQueue.dequeue(60000);
} catch (err) {
// ignore timeout
}
}
res.write(JSON.stringify(response));
res.end();
});
// open terminal
let shellPath: string = path.join(ext.context.asAbsolutePath('bin'), `node.${isWindows ? 'bat' : 'sh'}`);
let cloudConsoleLauncherPath: string = path.join(ext.context.asAbsolutePath('dist'), 'cloudConsoleLauncher');
if (isWindows) {
cloudConsoleLauncherPath = cloudConsoleLauncherPath.replace(/\\/g, '\\\\');
}
const shellArgs: string[] = [
process.argv0,
'-e',
`require('${cloudConsoleLauncherPath}').main()`,
];
if (isWindows) {
// Work around https://github.com/electron/electron/issues/4218 https://github.com/nodejs/node/issues/11656
shellPath = 'node.exe';
shellArgs.shift();
}
// Only add flag if in Electron process https://github.com/microsoft/vscode-azure-account/pull/684
// if (!isWindows && !!process.versions['electron'] && env.uiKind === UIKind.Desktop && semver.gte(version, '1.62.1')) {
// // https://github.com/microsoft/vscode/issues/136987
// // This fix can't be applied to all versions of VS Code. An error is thrown in versions less than the one specified
// shellArgs.push('--ms-enable-electron-run-as-node');
// }
const terminalOptions: TerminalOptions = {
name: localize('azureCloudShell', 'Azure Cloud Shell ({0})', os.shellName),
iconPath: new ThemeIcon('azure'),
shellPath,
shellArgs,
env: {
CLOUD_CONSOLE_IPC: server.ipcHandlePath,
},
isTransient: true
};
const cleanupCloudShell = () => {
liveServerQueue = undefined;
server.dispose();
updateStatus('Disconnected');
}
// Open the appropriate type of VS Code terminal depending on the entry point
if (terminalProfileToken) {
// Entry point: Terminal profile provider
const terminalProfileCloseSubscription = terminalProfileToken.onCancellationRequested(() => {
terminalProfileCloseSubscription.dispose();
cleanupCloudShell();
});
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
deferredTerminalProfile!.resolve(new TerminalProfile(terminalOptions));
} else {
// Entry point: Extension API
const terminal: Terminal = window.createTerminal(terminalOptions);
const terminalCloseSubscription = window.onDidCloseTerminal(t => {
if (t === terminal) {
terminalCloseSubscription.dispose();
cleanupCloudShell();
}
});
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
deferredTerminal!.resolve(terminal);
}
liveServerQueue = serverQueue;
// TODO: handle not signed in case
// if (await authProvider.isSignedIn()) {
// if (loginStatus === 'LoggingIn') {
// serverQueue.push({ type: 'log', args: [localize('azure-account.loggingIn', "Signing in...")] });
// }
// if (!(await api.waitForLogin())) {
// serverQueue.push({ type: 'log', args: [localize('azure-account.loginNeeded', "Sign in needed.")] });
// context.telemetry.properties.outcome = 'requiresLogin';
// await commands.executeCommand('azure-account.askForLogin');
// if (!(await api.waitForLogin())) {
// serverQueue.push({ type: 'exit' });
// updateStatus('Disconnected');
// return;
// }
// }
// }
const session = await authentication.getSession('microsoft', ['https://management.core.windows.net//.default'], {
createIfNone: false,
});
const result = session && await findUserSettings(session.accessToken);
if (!result) {
serverQueue.push({ type: 'log', args: [localize('azure-account.setupNeeded', "Setup needed.")] });
await requiresSetUp(context);
serverQueue.push({ type: 'exit' });
updateStatus('Disconnected');
return;
}
// provision
let consoleUri: string;
const provisionTask: () => Promise<void> = async () => {
consoleUri = await provisionConsole(session.accessToken, result, OSes.Linux.id);
context.telemetry.properties.outcome = 'provisioned';
}
try {
serverQueue.push({ type: 'log', args: [localize('azure-account.requestingCloudConsole', "Requesting a Cloud Shell...")] });
await provisionTask();
} catch (err) {
if (parseError(err).message === Errors.DeploymentOsTypeConflict) {
const reset = await deploymentConflict(context, os);
if (reset) {
await resetConsole(session.accessToken, getArmEndpoint());
return provisionTask();
} else {
serverQueue.push({ type: 'exit' });
updateStatus('Disconnected');
return;
}
} else {
throw err;
}
}
// Connect to terminal
const connecting: string = localize('azure-account.connectingTerminal', "Connecting terminal...");
serverQueue.push({ type: 'log', args: [connecting] });
const progressTask: (i: number) => void = (i: number) => {
serverQueue.push({ type: 'log', args: [`\x1b[A${connecting}${'.'.repeat(i)}`] });
};
const initialSize: Size = await initialSizePromise;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const consoleUris: ConsoleUris = await connectTerminal(session.accessToken, consoleUri!, /* TODO: Separate Shell from OS */ osName === 'Linux' ? 'bash' : 'pwsh', initialSize, progressTask);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
deferredUris!.resolve(consoleUris);
// Connect to WebSocket
serverQueue.push({
type: 'connect',
accessToken: session.accessToken,
consoleUris
});
})().catch(err => {
const parsedError: IParsedError = parseError(err);
ext.outputChannel.appendLog(parsedError.message);
parsedError.stack && ext.outputChannel.appendLog(parsedError.stack);
updateStatus('Disconnected');
context.telemetry.properties.outcome = 'error';
context.telemetry.properties.message = parsedError.message;
if (liveServerQueue) {
liveServerQueue.push({ type: 'log', args: [localize('azure-account.error', "Error: {0}", parsedError.message)] });
}
});
return state;
}))!;
}
async function findUserSettings(accessToken: string): Promise<UserSettings | undefined> {
const userSettings: UserSettings | undefined = await getUserSettings(accessToken);
// Valid settings will have either a storage profile (mounted) or a session type of 'Ephemeral'.
if (userSettings && (userSettings.storageProfile || userSettings.sessionType === 'Ephemeral')) {
return userSettings;
}
return undefined;
}
async function requiresSetUp(context: IActionContext) {
context.telemetry.properties.outcome = 'requiresSetUp';
const open: MessageItem = { title: localize('azure-account.open', "Open") };
const message: string = localize('azure-account.setUpInWeb', "First launch of Cloud Shell in a directory requires setup in the web application (https://shell.azure.com).");
const response: MessageItem | undefined = await window.showInformationMessage(message, open);
if (response === open) {
context.telemetry.properties.outcome = 'requiresSetUpOpen';
void env.openExternal(Uri.parse('https://shell.azure.com'));
} else {
context.telemetry.properties.outcome = 'requiresSetUpCancel';
}
}
async function requiresNode(context: IActionContext) {
context.telemetry.properties.outcome = 'requiresNode';
const open: MessageItem = { title: localize('azure-account.open', "Open") };
const message: string = localize('azure-account.requiresNode', "Opening a Cloud Shell currently requires Node.js 6 or later to be installed (https://nodejs.org).");
const response: MessageItem | undefined = await window.showInformationMessage(message, open);
if (response === open) {
context.telemetry.properties.outcome = 'requiresNodeOpen';
void env.openExternal(Uri.parse('https://nodejs.org'));
} else {
context.telemetry.properties.outcome = 'requiresNodeCancel';
}
}
async function requiresWorkspaceTrust(context: IActionContext) {
context.telemetry.properties.outcome = 'requiresWorkspaceTrust';
const ok: MessageItem = { title: localize('azure-account.ok', "OK") };
const message: string = localize('azure-account.cloudShellRequiresTrustedWorkspace', 'Opening a Cloud Shell only works in a trusted workspace.');
return await window.showInformationMessage(message, ok) === ok;
}
async function deploymentConflict(context: IActionContext, os: OS) {
context.telemetry.properties.outcome = 'deploymentConflict';
const ok: MessageItem = { title: localize('azure-account.ok', "OK") };
const message: string = localize('azure-account.deploymentConflict', "Starting a {0} session will terminate all active {1} sessions. Any running processes in active {1} sessions will be terminated.", os.shellName, os.otherOS.shellName);
const response: MessageItem | undefined = await window.showWarningMessage(message, ok);
const reset: boolean = response === ok;
context.telemetry.properties.outcome = reset ? 'deploymentConflictReset' : 'deploymentConflictCancel';
return reset;
}
// interface TenantDetails {
// objectId: string;
// displayName: string;
// domains: string;
// defaultDomain: string;
// }
// async function fetchTenantDetails(accessToken: string): Promise<{ session: AzureSession, tenantDetails: TenantDetails }> {
// const response: Response = await fetchWithLogging('https://management.azure.com/tenants?api-version=2022-12-01', {
// headers: {
// Authorization: `Bearer ${accessToken}`,
// "x-ms-client-request-id": uuid(),
// "Content-Type": 'application/json; charset=utf-8'
// }
// });
// // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
// const json = await response.json();
// return {
// session,
// // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
// tenantDetails: json.value[0]
// };
// }
export interface ExecResult {
error: Error | null;
stdout: string;
stderr: string;
}
async function exec(command: string): Promise<ExecResult> {
return new Promise<ExecResult>((resolve, reject) => {
cp.exec(command, (error, stdout, stderr) => {
(error || stderr ? reject : resolve)({ error, stdout, stderr });
});
});
}
const consoleApiVersion = '2023-02-01-preview';
export enum Errors {
DeploymentOsTypeConflict = 'DeploymentOsTypeConflict'
}
function getConsoleUri(armEndpoint: string) {
return `${armEndpoint}/providers/Microsoft.Portal/consoles/default?api-version=${consoleApiVersion}`;
}
export interface UserSettings {
preferredLocation: string;
preferredOsType: string; // The last OS chosen in the portal.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
storageProfile: any;
sessionType: 'Ephemeral' | 'Mounted';
}
export interface AccessTokens {
resource: string;
// graph: string;
keyVault?: string;
}
export interface ConsoleUris {
consoleUri: string;
terminalUri: string;
socketUri: string;
}
export interface Size {
cols: number;
rows: number;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function requestWithLogging(requestOptions: request.Options): Promise<any> {
try {
const requestLogger = new HttpLogger(ext.outputChannel, 'CloudConsoleLauncher', new RequestNormalizer());
requestLogger.logRequest(requestOptions);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const response: http.IncomingMessage & { body: unknown } = await request(requestOptions);
requestLogger.logResponse({ response, request: requestOptions });
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return response;
} catch (e) {
const error = parseError(e);
ext.outputChannel.error({ ...error, name: 'Request Error: CloudConsoleLauncher' });
}
}
export async function getUserSettings(accessToken: string): Promise<UserSettings | undefined> {
// TODO: ensure ending slash on armEndpoint
const targetUri = `${getConfiguredAzureEnv().resourceManagerEndpointUrl}/providers/Microsoft.Portal/userSettings/cloudconsole?api-version=${consoleApiVersion}`;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const response = await requestWithLogging({
uri: targetUri,
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
simple: false,
resolveWithFullResponse: true,
json: true,
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (response.statusCode < 200 || response.statusCode > 299) {
// if (response.body && response.body.error && response.body.error.message) {
// console.log(`${response.body.error.message} (${response.statusCode})`);
// } else {
// console.log(response.statusCode, response.headers, response.body);
// }
return;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
return response.body && response.body.properties;
}
/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
export async function provisionConsole(accessToken: string, userSettings: UserSettings, osType: string): Promise<string> {
let response = await createTerminal(accessToken, userSettings, osType, true);
for (let i = 0; i < 10; i++, response = await createTerminal(accessToken, userSettings, osType, false)) {
if (response.statusCode < 200 || response.statusCode > 299) {
if (response.statusCode === 409 && response.body && response.body.error && response.body.error.code === Errors.DeploymentOsTypeConflict) {
throw new Error(Errors.DeploymentOsTypeConflict);
} else if (response.body && response.body.error && response.body.error.message) {
throw new Error(`${response.body.error.message} (${response.statusCode})`);
} else {
throw new Error(`${response.statusCode} ${response.headers} ${response.body}`);
}
}
const consoleResource = response.body;
if (consoleResource.properties.provisioningState === 'Succeeded') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return consoleResource.properties.uri;
} else if (consoleResource.properties.provisioningState === 'Failed') {
break;
}
}
throw new Error(`Sorry, your Cloud Shell failed to provision. Please retry later. Request correlation id: ${response.headers['x-ms-routing-request-id']}`);
}
/* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
async function createTerminal(accessToken: string, userSettings: UserSettings, osType: string, initial: boolean) {
return requestWithLogging({
uri: getConsoleUri(getArmEndpoint()),
method: initial ? 'PUT' : 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
'x-ms-console-preferred-location': userSettings.preferredLocation
},
simple: false,
resolveWithFullResponse: true,
json: true,
body: initial ? {
properties: {
osType
}
} : undefined
});
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export async function resetConsole(accessToken: string, armEndpoint: string) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const response = await requestWithLogging({
uri: getConsoleUri(armEndpoint),
method: 'DELETE',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
simple: false,
resolveWithFullResponse: true,
json: true
});
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
if (response.statusCode < 200 || response.statusCode > 299) {
if (response.body && response.body.error && response.body.error.message) {
throw new Error(`${response.body.error.message} (${response.statusCode})`);
} else {
throw new Error(`${response.statusCode} ${response.headers} ${response.body}`);
}
}
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
}
export async function connectTerminal(accessToken: string, consoleUri: string, shellType: string, initialSize: Size, progress: (i: number) => void): Promise<ConsoleUris> {
for (let i = 0; i < 10; i++) {
/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
const response = await initializeTerminal(accessToken, consoleUri, shellType, initialSize);
if (response.statusCode < 200 || response.statusCode > 299) {
if (response.statusCode !== 503 && response.statusCode !== 504 && response.body && response.body.error) {
if (response.body && response.body.error && response.body.error.message) {
throw new Error(`${response.body.error.message} (${response.statusCode})`);
} else {
throw new Error(`${response.statusCode} ${response.headers} ${response.body}`);
}
}
await delay(1000 * (i + 1));
progress(i + 1);
continue;
}
const { id, socketUri } = response.body;
const terminalUri = `${consoleUri}/terminals/${id}`;
return {
consoleUri,
terminalUri,
socketUri
};
/* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
}
throw new Error('Failed to connect to the terminal.');
}
async function initializeTerminal(accessToken: string, consoleUri: string, shellType: string, initialSize: Size) {
const consoleUrl = new URL(consoleUri);
return requestWithLogging({
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
uri: consoleUri + '/terminals?cols=' + initialSize.cols + '&rows=' + initialSize.rows + '&shell=' + shellType,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${accessToken}`,
'Referer': consoleUrl.protocol + "//" + consoleUrl.hostname + '/$hc' + consoleUrl.pathname + '/terminals',
},
simple: false,
resolveWithFullResponse: true,
json: true,
body: {
tokens: []
}
});
}

Просмотреть файл

@ -0,0 +1,200 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// this file is run as a child process from the extension host and communicates via IPC
import * as http from 'http';
import { HttpProxyAgent } from 'http-proxy-agent';
import { HttpsProxyAgent } from 'https-proxy-agent';
import * as request from 'request-promise';
import * as WS from 'ws';
import { readJSON, sendData } from './ipcUtils';
export function delay<T = void>(ms: number, result?: T | PromiseLike<T>): Promise<T | PromiseLike<T> | undefined> {
return new Promise(resolve => setTimeout(() => resolve(result), ms));
}
export interface AccessTokens {
resource: string;
graph: string;
keyVault?: string;
}
export interface ConsoleUris {
consoleUri: string;
terminalUri: string;
socketUri: string;
}
export interface Size {
cols: number;
rows: number;
}
function getWindowSize(): Size {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const stdout: any = process.stdout;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
const windowSize: [number, number] = stdout.isTTY ? stdout.getWindowSize() : [80, 30];
return {
cols: windowSize[0],
rows: windowSize[1],
};
}
let resizeToken = {};
async function resize(accessToken: string, terminalUri: string) {
const token = resizeToken = {};
await delay(300);
for (let i = 0; i < 10; i++) {
if (token !== resizeToken) {
return;
}
const { cols, rows } = getWindowSize();
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const response = await request({
uri: `${terminalUri}/size?cols=${cols}&rows=${rows}`,
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
simple: false,
resolveWithFullResponse: true,
json: true,
// Provide empty body so that 'Content-Type' header is set properly
body: {}
});
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
if (response.statusCode < 200 || response.statusCode > 299) {
if (response.statusCode !== 503 && response.statusCode !== 504 && response.body && response.body.error) {
if (response.body && response.body.error && response.body.error.message) {
console.log(`${response.body.error.message} (${response.statusCode})`);
} else {
console.log(response.statusCode, response.headers, response.body);
}
break;
}
await delay(1000 * (i + 1));
continue;
}
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
return;
}
console.log('Failed to resize terminal.');
}
function connectSocket(ipcHandle: string, url: string) {
const proxy = process.env.HTTPS_PROXY || process.env.HTTP_PROXY || undefined;
let agent: http.Agent | undefined = undefined;
if (proxy) {
agent = url.startsWith('ws:') || url.startsWith('http:') ? new HttpProxyAgent(proxy) : new HttpsProxyAgent(proxy);
}
const ws = new WS(url, {
agent
});
ws.on('open', function () {
process.stdin.on('data', function (data) {
ws.send(data);
});
startKeepAlive();
sendData(ipcHandle, JSON.stringify([{ type: 'status', status: 'Connected' }]))
.catch(err => {
console.error(err);
});
});
ws.on('message', function (data) {
process.stdout.write(String(data));
});
let error = false;
ws.on('error', function (event) {
error = true;
console.error('Socket error: ' + JSON.stringify(event));
});
ws.on('close', function () {
console.log('Socket closed');
sendData(ipcHandle, JSON.stringify([{ type: 'status', status: 'Disconnected' }]))
.catch(err => {
console.error(err);
});
if (!error) {
process.exit(0);
}
});
function startKeepAlive() {
let isAlive = true;
ws.on('pong', () => {
isAlive = true;
});
const timer = setInterval(() => {
if (isAlive === false) {
error = true;
console.log('Socket timeout');
ws.terminate();
clearInterval(timer);
} else {
isAlive = false;
ws.ping();
}
}, 60000);
timer.unref();
}
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function main() {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
process.stdin.setRawMode!(true);
process.stdin.resume();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const ipcHandle = process.env.CLOUD_CONSOLE_IPC!;
(async () => {
void sendData(ipcHandle, JSON.stringify([{ type: 'size', size: getWindowSize() }]));
let res: http.IncomingMessage;
// eslint-disable-next-line no-cond-assign
while (res = await sendData(ipcHandle, JSON.stringify([{ type: 'poll' }]))) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
for (const message of await readJSON<any>(res)) {
/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
if (message.type === 'log') {
console.log(...(message.args) as []);
} else if (message.type === 'connect') {
try {
const accessToken: string = message.accessToken;
const consoleUris: ConsoleUris = message.consoleUris;
connectSocket(ipcHandle, consoleUris.socketUri);
process.stdout.on('resize', () => {
resize(accessToken, consoleUris.terminalUri)
.catch(console.error);
});
} catch (err) {
console.error(err);
sendData(ipcHandle, JSON.stringify([{ type: 'status', status: 'Disconnected' }]))
.catch(err => {
console.error(err);
});
}
} else if (message.type === 'exit') {
process.exit(message.code as number);
}
/* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
}
}
})()
.catch(console.error);
}

Просмотреть файл

@ -0,0 +1,21 @@
import { parseError } from "@microsoft/vscode-azext-utils";
import { localize } from "vscode-nls";
import { ext } from "../extensionVariables";
export function delay<T = void>(ms: number, result?: T | PromiseLike<T>): Promise<T | PromiseLike<T> | undefined> {
return new Promise(resolve => setTimeout(() => resolve(result), ms));
}
export interface Deferred<T> {
resolve: (result: T | Promise<T>) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
reject: (reason: any) => void;
}
export function logErrorMessage(error: unknown): void {
ext.outputChannel.error(parseError(error).message);
}
export function logAttemptingToReachUrlMessage(url: string): void {
ext.outputChannel.appendLog(localize('azure-account.attemptingToReachUrl', 'Attempting to reach URL "{0}"...', url));
}

103
src/cloudConsole/ipc.ts Normal file
Просмотреть файл

@ -0,0 +1,103 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as crypto from 'crypto';
import * as http from 'http';
import * as os from 'os';
import * as path from 'path';
import { ext } from '../extensionVariables';
export async function createServer(ipcHandlePrefix: string, onRequest: http.RequestListener): Promise<Server> {
const buffer = await randomBytes(20);
const nonce = buffer.toString('hex');
const ipcHandlePath = getIPCHandlePath(`${ipcHandlePrefix}-${nonce}`);
const server = new Server(ipcHandlePath, onRequest);
server.listen();
return server;
}
export class Server {
public server: http.Server;
constructor(public ipcHandlePath: string, onRequest: http.RequestListener) {
this.server = http.createServer((req, res) => {
Promise.resolve(onRequest(req, res))
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
.catch((err) => console.error(err && err.message || err));
});
this.server.on('error', err => console.error(err));
}
listen(): void {
this.server.listen(this.ipcHandlePath);
}
dispose(): void {
this.server.close(error => error && error.message && ext.outputChannel.error(error.message));
}
}
async function randomBytes(size: number) {
return new Promise<Buffer>((resolve, reject) => {
crypto.randomBytes(size, (err, buf) => {
if (err) {
reject(err);
} else {
resolve(buf);
}
});
});
}
function getIPCHandlePath(id: string): string {
if (process.platform === 'win32') {
return `\\\\.\\pipe\\${id}-sock`;
}
if (process.env['XDG_RUNTIME_DIR']) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return path.join(process.env['XDG_RUNTIME_DIR']!, `${id}.sock`);
}
return path.join(os.tmpdir(), `${id}.sock`);
}
export class Queue<T> {
private messages: T[] = [];
private dequeueRequest?: {
resolve: (value: T[]) => void;
reject: (err: unknown) => void;
};
public push(message: T): void {
this.messages.push(message);
if (this.dequeueRequest) {
this.dequeueRequest.resolve(this.messages);
this.dequeueRequest = undefined;
this.messages = [];
}
}
public async dequeue(timeout?: number): Promise<T[]> {
if (this.messages.length) {
const messages = this.messages;
this.messages = [];
return messages;
}
if (this.dequeueRequest) {
this.dequeueRequest.resolve([]);
}
return new Promise<T[]>((resolve, reject) => {
this.dequeueRequest = { resolve, reject };
if (typeof timeout === 'number') {
setTimeout(reject, timeout);
}
});
}
}

Просмотреть файл

@ -0,0 +1,32 @@
import * as http from 'http';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export async function readJSON<T>(req: http.IncomingMessage): Promise<any> {
return new Promise<T>((resolve, reject) => {
const chunks: string[] = [];
req.setEncoding('utf8');
req.on('data', (d: string) => chunks.push(d));
req.on('error', (err: Error) => reject(err));
req.on('end', () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const data = JSON.parse(chunks.join(''));
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
resolve(data);
});
});
}
export async function sendData(socketPath: string, data: string): Promise<http.IncomingMessage> {
return new Promise<http.IncomingMessage>((resolve, reject) => {
const opts: http.RequestOptions = {
socketPath,
path: '/',
method: 'POST'
};
const req = http.request(opts, res => resolve(res));
req.on('error', (err: Error) => reject(err));
req.write(data);
req.end();
});
}

Просмотреть файл

@ -0,0 +1,86 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { parseError } from "@microsoft/vscode-azext-utils";
import { LogLevel, LogOutputChannel } from "vscode";
import { DebugHttpStringifier, HttpStringifier, SimpleHttpStringifier, TraceHttpStringifier } from "./stringifyHttp";
/**
* A normalized HTTP request for logging.
*/
export interface NormalizedHttpRequest {
method?: string;
url?: string;
headers?: Record<string, unknown>;
proxy?: {
host?: string;
port?: string;
protocol?: string;
password?: string;
username?: string;
};
query?: Record<string, string>;
}
/**
* A normalized HTTP response for logging.
*/
export interface NormalizedHttpResponse {
status?: number;
bodyAsText?: string;
headers?: Record<string, unknown>;
request: NormalizedHttpRequest;
}
export interface HttpNormalizer<TRequest, TResponse> {
normalizeRequest(request: TRequest): NormalizedHttpRequest;
normalizeResponse(response: TResponse): NormalizedHttpResponse;
}
export class HttpLogger<TRequest, TResponse> implements HttpLogger<TRequest, TResponse> {
constructor(private readonly logOutputChannel: LogOutputChannel, private readonly source: string, private readonly normalizer: HttpNormalizer<TRequest, TResponse>) {}
logRequest(request: TRequest): void {
try {
this.logNormalizedRequest(this.normalizer.normalizeRequest(request));
} catch (e) {
const error = parseError(e);
this.logOutputChannel.error('Error logging request: ' + error.message);
}
}
logResponse(response: TResponse): void {
try {
this.logNormalizedResponse(this.normalizer.normalizeResponse(response));
} catch (e) {
const error = parseError(e);
this.logOutputChannel.error('Error logging response: ' + error.message);
}
}
private logNormalizedRequest(request: NormalizedHttpRequest): void {
if (request.proxy) {
// if proxy is configured, always log proxy configuration
this.logOutputChannel.info(new DebugHttpStringifier().stringifyRequest(request, this.source));
} else {
this.logOutputChannel.info(this.getStringifier().stringifyRequest(request, this.source));
}
}
private logNormalizedResponse(response: NormalizedHttpResponse): void {
this.logOutputChannel.info(this.getStringifier().stringifyResponse(response, this.source));
}
getStringifier(): HttpStringifier {
switch(this.logOutputChannel.logLevel) {
case LogLevel.Debug:
return new DebugHttpStringifier();
case LogLevel.Trace:
return new TraceHttpStringifier();
default:
return new SimpleHttpStringifier();
}
}
}

Просмотреть файл

@ -0,0 +1,36 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type { Headers, Request, Response } from "node-fetch";
import type { HttpNormalizer, NormalizedHttpRequest, NormalizedHttpResponse } from "../HttpLogger";
type NodeFetchResponseInfo = {response: Response, request: Request, bodyAsText: string };
export class NodeFetchNormalizer implements HttpNormalizer<Request, NodeFetchResponseInfo> {
normalizeRequest(request: Request): NormalizedHttpRequest {
return {
method: request.method,
url: request.url,
headers: this.convertNodeFetchHeaders(request.headers),
};
}
normalizeResponse(info: NodeFetchResponseInfo): NormalizedHttpResponse {
return {
request: this.normalizeRequest(info.request),
bodyAsText: info.bodyAsText,
status: info.response.status,
headers: this.convertNodeFetchHeaders(info.response.headers),
}
}
private convertNodeFetchHeaders(headers: Headers): Record<string, string> {
const headersRecord: Record<string, string> = {};
Object.entries(headers.raw()).forEach(([key, value]) => {
headersRecord[key] = value.join(', ');
});
return headersRecord;
}
}

Просмотреть файл

@ -0,0 +1,13 @@
import fetch, { Request, RequestInfo, RequestInit, Response } from "node-fetch";
import { ext } from "../../../extensionVariables";
import { HttpLogger } from "../HttpLogger";
import { NodeFetchNormalizer } from "./NodeFetchNormalizer";
export async function fetchWithLogging(url: RequestInfo, init?: RequestInit): Promise<Response> {
const nodeFetchLogger = new HttpLogger(ext.outputChannel, 'NodeFetch', new NodeFetchNormalizer());
const request = new Request(url, init);
nodeFetchLogger.logRequest(request);
const response = await fetch(url, init);
nodeFetchLogger.logResponse({ response, request, bodyAsText: await response.clone().text() });
return response;
}

Просмотреть файл

@ -0,0 +1,28 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as http from 'http';
import * as request from 'request-promise';
import type { HttpNormalizer, NormalizedHttpRequest, NormalizedHttpResponse } from "../HttpLogger";
type RequestResponseInfo = { response: http.IncomingMessage & { body?: unknown }, request: request.Options };
export class RequestNormalizer implements HttpNormalizer<request.Options, RequestResponseInfo> {
normalizeRequest(options: request.Options): NormalizedHttpRequest {
return {
...options,
url: 'url' in options ? options.url.toString() : options.uri.toString()
};
}
normalizeResponse(info: RequestResponseInfo): NormalizedHttpResponse {
return {
request: this.normalizeRequest(info.request),
status: info.response.statusCode,
headers: info.response.headers,
bodyAsText: 'body' in info.response ? JSON.stringify(info.response.body) : undefined,
}
}
}

Просмотреть файл

@ -0,0 +1,88 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { NormalizedHttpRequest, NormalizedHttpResponse } from "./HttpLogger";
export interface HttpStringifier {
stringifyRequest(request: NormalizedHttpRequest, source: string): string;
stringifyResponse(request: NormalizedHttpRequest, source: string): string;
}
/**
* @param request Request to log
* @param source Source of the request, ex: Axios
* @param verbose Logs the headers and query parameters if true
* @returns stringified request
*/
function stringifyRequest(request: NormalizedHttpRequest, source: string, verbose?: boolean): string {
let message = `[${source} Request]`;
message = `\n┌────── ${source} Request ${request.method} ${request.url}`;
if (verbose) {
message += stringifyRecord(request.headers ?? {}, 'Headers');
message += stringifyRecord(request.query ?? {}, 'Query parameters');
}
message += stringifyRecord(request.proxy ?? {}, 'Proxy configuration', true);
message += `\n└───────────────────────────────────────────────────`;
return message;
}
/**
*
* @param response Response to log
* @param source Source of the response, ex: Axios
* @param hideBody Hides the body and prints the string instead
* @returns stringified request
*/
function stringifyResponse(response: NormalizedHttpResponse, source: string, hideBody?: string): string {
let message = `[${source} Response]`;
message += `\n┌────── ${source} Response ${response.request.method} - ${response.request.url}`;
message += stringifyRecord(response.headers ?? {}, 'Headers');
// only show the body if the log level is trace
message += `\n\tBody: ${hideBody ?? `\n\t${response.bodyAsText?.split('\n').join('\n\t')}`}`;
message += `\n└───────────────────────────────────────────────────`;
return message;
}
function stringifyRecord(record: Record<string, unknown>, label: string, hideCount?: boolean): string {
const entries = Object.entries(record).sort().filter(([_, value]) => typeof value !== 'object');
const entriesString = '\n\t└ ' + entries.map(([name, value]) => `${name}: "${Array.isArray(value) ? value.join(', ') : String(value)}"`).join('\n\t└ ');
return `\n\t${label}${entries.length && !hideCount ? ` (${entries.length})` : ''}:${entries.length === 0 ? ' None' : entriesString}`;
}
export class SimpleHttpStringifier implements HttpStringifier {
stringifyRequest(request: NormalizedHttpRequest, source: string): string {
return `[${source} Request] ${request.method} ${request.url}`;
}
stringifyResponse(response: NormalizedHttpResponse, source: string): string {
return `[${source} Response] ${response.status} - ${response.request.method} ${response.request.url}`;
}
}
export class DebugHttpStringifier implements HttpStringifier {
stringifyRequest(request: NormalizedHttpRequest, source: string): string {
return stringifyRequest(request, source, false);
}
stringifyResponse(response: NormalizedHttpResponse, source: string): string {
return stringifyResponse(response, source, "Hidden. Set log level to 'Trace' to see body.");
}
protected stringifyRecord(record: Record<string, unknown>, label: string, hideCount?: boolean): string {
const entries = Object.entries(record).sort().filter(([_, value]) => typeof value !== 'object');
const entriesString = '\n\t└ ' + entries.map(([name, value]) => `${name}: "${Array.isArray(value) ? value.join(', ') : String(value)}"`).join('\n\t└ ');
return `\n\t${label}${entries.length && !hideCount ? ` (${entries.length})` : ''}:${entries.length === 0 ? ' None' : entriesString}`;
}
}
export class TraceHttpStringifier implements HttpStringifier {
stringifyRequest(request: NormalizedHttpRequest, source: string): string {
return stringifyRequest(request, source, true);
}
stringifyResponse(response: NormalizedHttpResponse, source: string): string {
return stringifyResponse(response, source);
}
}

Просмотреть файл

@ -22,6 +22,7 @@ import { registerApplicationResourceResolver } from './api/compatibility/registe
import { registerWorkspaceResourceProvider } from './api/compatibility/registerWorkspaceResourceProvider';
import { createAzureResourcesHostApi } from './api/createAzureResourcesHostApi';
import { createWrappedAzureResourcesExtensionApi } from './api/createWrappedAzureResourcesExtensionApi';
import { createCloudConsole } from './cloudConsole/cloudConsole';
import { registerCommands } from './commands/registerCommands';
import { TagFileSystem } from './commands/tags/TagFileSystem';
import { registerTagDiagnostics } from './commands/tags/registerTagDiagnostics';
@ -84,6 +85,17 @@ export async function activate(context: vscode.ExtensionContext, perfStats: { lo
ext.activityLogTree = new AzExtTreeDataProvider(ext.activityLogTreeItem, 'azureActivityLog.loadMore');
context.subscriptions.push(vscode.window.createTreeView('azureActivityLog', { treeDataProvider: ext.activityLogTree }));
context.subscriptions.push(vscode.window.registerTerminalProfileProvider('azureResources.cloudShellBash', {
provideTerminalProfile: async (token: vscode.CancellationToken) => {
return createCloudConsole(await ext.subscriptionProviderFactory(), 'Linux', token).terminalProfile;
}
}));
context.subscriptions.push(vscode.window.registerTerminalProfileProvider('azureResources.cloudShellPowerShell', {
provideTerminalProfile: async (token: vscode.CancellationToken) => {
return createCloudConsole(await ext.subscriptionProviderFactory(), 'Windows', token).terminalProfile;
}
}));
registerCommands();
});

Просмотреть файл

@ -0,0 +1,26 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
/**
* Returns a node module installed with VSCode, or undefined if it fails.
*/
export function getCoreNodeModule<T>(moduleName: string): T | undefined {
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return require(`${vscode.env.appRoot}/node_modules.asar/${moduleName}`);
} catch (err) {
// ignore
}
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return require(`${vscode.env.appRoot}/node_modules/${moduleName}`);
} catch (err) {
// ignore
}
return undefined;
}

Просмотреть файл

@ -4,7 +4,8 @@
"target": "es6",
"outDir": "out",
"lib": [
"es6"
"es6",
"DOM"
],
"sourceMap": true,
"rootDir": ".",
@ -12,6 +13,7 @@
"noUnusedLocals": true,
"noImplicitReturns": true,
"noUnusedParameters": true,
"allowSyntheticDefaultImports": true,
"baseUrl": "./",
"paths": {
"*": [

Просмотреть файл

@ -12,12 +12,16 @@
const process = require('process');
const dev = require("@microsoft/vscode-azext-dev");
const webpack = require('webpack');
const CopyWebpackPlugin = require('copy-webpack-plugin');
let DEBUG_WEBPACK = !/^(false|0)?$/i.test(process.env.DEBUG_WEBPACK || '');
const config = dev.getDefaultWebpackConfig({
projectRoot: __dirname,
verbosity: DEBUG_WEBPACK ? 'debug' : 'normal',
entries: {
cloudConsoleLauncher: './src/cloudConsole/cloudConsoleLauncher.ts',
},
externals:
{
// Fix "Module not found" errors in ./node_modules/websocket/lib/{BufferUtil,Validation}.js
@ -27,35 +31,25 @@ const config = dev.getDefaultWebpackConfig({
'../build/default/validation': 'commonjs ../build/default/validation',
'../build/Release/bufferutil': 'commonjs ../build/Release/bufferutil',
'../build/default/bufferutil': 'commonjs ../build/default/bufferutil',
// for cloud shell
bufferutil: 'commonjs bufferutil',
'utf-8-validate': 'commonjs utf-8-validate',
'./platform/openbsd': 'commonjs copy-paste-openbsd',
},
target: 'node',
suppressCleanDistFolder: true
});
const webConfig = dev.getDefaultWebpackConfig({
projectRoot: __dirname,
verbosity: DEBUG_WEBPACK ? 'debug' : 'normal',
externals:
{
// Fix "Module not found" errors in ./node_modules/websocket/lib/{BufferUtil,Validation}.js
// These files are not in node_modules and so will fail normally at runtime and instead use fallbacks.
// Make them as external so webpack doesn't try to process them, and they'll simply fail at runtime as before.
'../build/Release/validation': 'commonjs ../build/Release/validation',
'../build/default/validation': 'commonjs ../build/default/validation',
'../build/Release/bufferutil': 'commonjs ../build/Release/bufferutil',
'../build/default/bufferutil': 'commonjs ../build/default/bufferutil',
},
target: 'webworker',
plugins: [
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
],
suppressCleanDistFolder: true
suppressCleanDistFolder: true,
// plugins: [
// new CopyWebpackPlugin({
// patterns: [
// { from: './out/src/utils/getCoreNodeModule.js', to: 'node_modules' }
// ]
// })
// ]
});
if (DEBUG_WEBPACK) {
console.log('Config:', config);
}
module.exports = [config, webConfig];
module.exports = [config];