export function createCloudConsole()

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;
	}))!;
}