Initial migration of Cloud Shell feature
This commit is contained in:
Родитель
152cbe37d5
Коммит
e468390050
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
28
package.json
28
package.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));
|
||||
}
|
|
@ -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];
|
||||
|
|
Загрузка…
Ссылка в новой задаче