in src/cloudConsole/cloudConsole.ts [160:474]
export function createCloudConsole(api: AzureAccountExtensionApi, 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];
// 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> {
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);
} else if (message.type === 'status') {
updateStatus(message.status);
}
/* 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();
}
if (process.platform === 'darwin' && 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,
}
};
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;
if (getAuthLibrary() === 'MSAL') {
serverQueue.push({ type: 'log', args: [localize('azure-account.doesNotSupportMsal', 'Cloud Shell does not currently support authenticating with MSAL. Please set the "azure.authenticationLibrary" setting to "ADAL" and try again.')] });
return;
}
const loginStatus: AzureLoginStatus = await waitForLoginStatus(api);
if (loginStatus !== 'LoggedIn') {
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;
}
}
}
let token: Token | undefined = undefined;
await api.waitForSubscriptions();
const sessions: AzureSession[] = [...new Set(api.subscriptions.map(subscription => subscription.session))]; // Only consider those with at least one subscription.
if (sessions.length > 1) {
serverQueue.push({ type: 'log', args: [localize('azure-account.selectDirectory', "Select directory...")] });
const fetchingDetails: Promise<({
session: AzureSession;
tenantDetails: TenantDetails;
} | undefined)[]> = Promise.all(sessions.map(session => fetchTenantDetails(<AzureSession>session)
.catch(err => {
logErrorMessage(err);
return undefined;
})))
.then(tenantDetails => tenantDetails.filter(details => details));
const pick = await window.showQuickPick<QuickPickItem & { session: AzureSession }>(fetchingDetails
.then(tenantDetails => tenantDetails.map(details => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const tenantDetails: TenantDetails = details!.tenantDetails;
const defaultDomainName: string | undefined = (tenantDetails.verifiedDomains.find(domain => domain.default))?.name;
return {
label: tenantDetails.displayName,
description: defaultDomainName,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
session: details!.session
};
}).sort((a, b) => a.label.localeCompare(b.label))), {
placeHolder: localize('azure-account.selectDirectoryPlaceholder', "Select directory"),
ignoreFocusOut: true // The terminal opens concurrently and can steal focus (https://github.com/microsoft/vscode-azure-account/issues/77).
});
if (!pick) {
context.telemetry.properties.outcome = 'noTenantPicked';
serverQueue.push({ type: 'exit' });
updateStatus('Disconnected');
return;
}
token = await acquireToken(pick.session);
} else if (sessions.length === 1) {
token = await acquireToken(<AzureSession>sessions[0]);
}
const result = token && await findUserSettings(token);
if (!result) {
serverQueue.push({ type: 'log', args: [localize('azure-account.setupNeeded', "Setup needed.")] });
await requiresSetUp(context);
serverQueue.push({ type: 'exit' });
updateStatus('Disconnected');
return;
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
deferredSession!.resolve(result.token.session);
// provision
let consoleUri: string;
const session: AzureSession = result.token.session;
const accessToken: string = result.token.accessToken;
const armEndpoint: string = session.environment.resourceManagerEndpointUrl;
const provisionTask: () => Promise<void> = async () => {
consoleUri = await provisionConsole(accessToken, armEndpoint, result.userSettings, 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(accessToken, armEndpoint);
return provisionTask();
} else {
serverQueue.push({ type: 'exit' });
updateStatus('Disconnected');
return;
}
} else {
throw err;
}
}
// Additional tokens
const [graphToken, keyVaultToken] = await Promise.all([
tokenFromRefreshToken(session.environment, result.token.refreshToken, session.tenantId, session.environment.activeDirectoryGraphResourceId),
session.environment.keyVaultDnsSuffix
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
? tokenFromRefreshToken(session.environment, result.token.refreshToken, session.tenantId, `https://${session.environment.keyVaultDnsSuffix!.substr(1)}`)
: Promise.resolve(undefined)
]);
const accessTokens: AccessTokens = {
resource: accessToken,
graph: graphToken.accessToken,
keyVault: keyVaultToken && keyVaultToken.accessToken
};
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
deferredTokens!.resolve(accessTokens);
// 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(accessTokens, 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',
accessTokens,
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;
}))!;
}