diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index ad961496bcc..a97e1fc3870 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -649,6 +649,10 @@ pub struct TunnelServiceInstallArgs { /// If set, the user accepts the server license terms and the server will be started without a user prompt. #[clap(long)] pub accept_server_license_terms: bool, + + /// Sets the machine name for port forwarding service + #[clap(long)] + pub name: Option, } #[derive(Args, Debug, Clone)] diff --git a/cli/src/commands/tunnels.rs b/cli/src/commands/tunnels.rs index d11c15ef1e3..24e349bb720 100644 --- a/cli/src/commands/tunnels.rs +++ b/cli/src/commands/tunnels.rs @@ -135,10 +135,17 @@ pub async fn service( let manager = create_service_manager(ctx.log.clone(), &ctx.paths); match service_args { TunnelServiceSubCommands::Install(args) => { - // ensure logged in, otherwise subsequent serving will fail - Auth::new(&ctx.paths, ctx.log.clone()) - .get_credential() - .await?; + let auth = Auth::new(&ctx.paths, ctx.log.clone()); + + if let Some(name) = &args.name { + // ensure the name matches, and tunnel exists + dev_tunnels::DevTunnels::new(&ctx.log, auth, &ctx.paths) + .rename_tunnel(name) + .await?; + } else { + // still ensure they're logged in, otherwise subsequent serving will fail + auth.get_credential().await?; + } // likewise for license consent legal::require_consent(&ctx.paths, args.accept_server_license_terms)?; @@ -203,20 +210,20 @@ pub async fn user(ctx: CommandContext, user_args: TunnelUserSubCommands) -> Resu Ok(0) } -/// Remove the tunnel used by this gateway, if any. +/// Remove the tunnel used by this tunnel, if any. pub async fn rename(ctx: CommandContext, rename_args: TunnelRenameArgs) -> Result { let auth = Auth::new(&ctx.paths, ctx.log.clone()); let mut dt = dev_tunnels::DevTunnels::new(&ctx.log, auth, &ctx.paths); dt.rename_tunnel(&rename_args.name).await?; ctx.log.result(format!( - "Successfully renamed this gateway to {}", + "Successfully renamed this tunnel to {}", &rename_args.name )); Ok(0) } -/// Remove the tunnel used by this gateway, if any. +/// Remove the tunnel used by this tunnel, if any. pub async fn unregister(ctx: CommandContext) -> Result { let auth = Auth::new(&ctx.paths, ctx.log.clone()); let mut dt = dev_tunnels::DevTunnels::new(&ctx.log, auth, &ctx.paths); diff --git a/cli/src/rpc.rs b/cli/src/rpc.rs index acd53dc38e0..a9a66153735 100644 --- a/cli/src/rpc.rs +++ b/cli/src/rpc.rs @@ -117,7 +117,7 @@ impl RpcMethodBuilder { Ok(p) => p, Err(err) => { return id.map(|id| { - serial.serialize(&ErrorResponse { + serial.serialize(ErrorResponse { id, error: ResponseError { code: 0, @@ -131,7 +131,7 @@ impl RpcMethodBuilder { match callback(param.params, &context) { Ok(result) => id.map(|id| serial.serialize(&SuccessResponse { id, result })), Err(err) => id.map(|id| { - serial.serialize(&ErrorResponse { + serial.serialize(ErrorResponse { id, error: ResponseError { code: -1, @@ -161,7 +161,7 @@ impl RpcMethodBuilder { Ok(p) => p, Err(err) => { return future::ready(id.map(|id| { - serial.serialize(&ErrorResponse { + serial.serialize(ErrorResponse { id, error: ResponseError { code: 0, @@ -182,7 +182,7 @@ impl RpcMethodBuilder { id.map(|id| serial.serialize(&SuccessResponse { id, result })) } Err(err) => id.map(|id| { - serial.serialize(&ErrorResponse { + serial.serialize(ErrorResponse { id, error: ResponseError { code: -1, @@ -222,7 +222,7 @@ impl RpcMethodBuilder { return ( None, future::ready(id.map(|id| { - serial.serialize(&ErrorResponse { + serial.serialize(ErrorResponse { id, error: ResponseError { code: 0, @@ -255,7 +255,7 @@ impl RpcMethodBuilder { match callback(servers, param.params, context).await { Ok(r) => id.map(|id| serial.serialize(&SuccessResponse { id, result: r })), Err(err) => id.map(|id| { - serial.serialize(&ErrorResponse { + serial.serialize(ErrorResponse { id, error: ResponseError { code: -1, @@ -427,7 +427,7 @@ impl RpcDispatcher { Some(Method::Async(callback)) => MaybeSync::Future(callback(id, body)), Some(Method::Duplex(callback)) => MaybeSync::Stream(callback(id, body)), None => MaybeSync::Sync(id.map(|id| { - self.serializer.serialize(&ErrorResponse { + self.serializer.serialize(ErrorResponse { id, error: ResponseError { code: -1, diff --git a/cli/src/tunnels/dev_tunnels.rs b/cli/src/tunnels/dev_tunnels.rs index 8476028a2f5..c4ca9741b88 100644 --- a/cli/src/tunnels/dev_tunnels.rs +++ b/cli/src/tunnels/dev_tunnels.rs @@ -275,7 +275,9 @@ impl DevTunnels { /// Renames the current tunnel to the new name. pub async fn rename_tunnel(&mut self, name: &str) -> Result<(), AnyError> { - self.update_tunnel_name(None, name).await.map(|_| ()) + self.update_tunnel_name(self.launcher_tunnel.load(), name) + .await + .map(|_| ()) } /// Updates the name of the existing persisted tunnel to the new name. @@ -286,28 +288,34 @@ impl DevTunnels { name: &str, ) -> Result<(Tunnel, PersistedTunnel), AnyError> { let name = name.to_ascii_lowercase(); - self.check_is_name_free(&name).await?; - - debug!(self.log, "Tunnel name changed, applying updates..."); let (mut full_tunnel, mut persisted, is_new) = match persisted { Some(persisted) => { + debug!( + self.log, + "Found a persisted tunnel, seeing if the name matches..." + ); self.get_or_create_tunnel(persisted, Some(&name), NO_REQUEST_OPTIONS) .await } - None => self - .create_tunnel(&name, NO_REQUEST_OPTIONS) - .await - .map(|(pt, t)| (t, pt, true)), + None => { + debug!(self.log, "Creating a new tunnel with the requested name"); + self.create_tunnel(&name, NO_REQUEST_OPTIONS) + .await + .map(|(pt, t)| (t, pt, true)) + } }?; - if is_new { + let desired_tags = self.get_tags(&name); + if is_new || vec_eq_as_set(&full_tunnel.tags, &desired_tags) { return Ok((full_tunnel, persisted)); } - full_tunnel.tags = self.get_tags(&name); + debug!(self.log, "Tunnel name changed, applying updates..."); - let new_tunnel = spanf!( + full_tunnel.tags = desired_tags; + + let updated_tunnel = spanf!( self.log, self.log.span("dev-tunnel.tag.update"), self.client.update_tunnel(&full_tunnel, NO_REQUEST_OPTIONS) @@ -317,7 +325,7 @@ impl DevTunnels { persisted.name = name; self.launcher_tunnel.save(Some(persisted.clone()))?; - Ok((new_tunnel, persisted)) + Ok((updated_tunnel, persisted)) } /// Gets the persisted tunnel from the service, or creates a new one. @@ -443,6 +451,8 @@ impl DevTunnels { ) -> Result<(PersistedTunnel, Tunnel), AnyError> { info!(self.log, "Creating tunnel with the name: {}", name); + self.check_is_name_free(name).await?; + let mut tried_recycle = false; let new_tunnel = Tunnel { @@ -527,7 +537,7 @@ impl DevTunnels { options: &TunnelRequestOptions, ) -> Result { let new_tags = self.get_tags(name); - if vec_eq_unsorted(&tunnel.tags, &new_tags) { + if vec_eq_as_set(&tunnel.tags, &new_tags) { return Ok(tunnel); } @@ -610,7 +620,7 @@ impl DevTunnels { } async fn check_is_name_free(&mut self, name: &str) -> Result<(), AnyError> { - let existing = spanf!( + let existing: Vec = spanf!( self.log, self.log.span("dev-tunnel.rename.search"), self.client.list_all_tunnels(&TunnelRequestOptions { @@ -998,7 +1008,7 @@ fn clean_hostname_for_tunnel(hostname: &str) -> String { } } -fn vec_eq_unsorted(a: &[String], b: &[String]) -> bool { +fn vec_eq_as_set(a: &[String], b: &[String]) -> bool { if a.len() != b.len() { return false; } diff --git a/cli/src/tunnels/service_windows.rs b/cli/src/tunnels/service_windows.rs index 427eddd620d..3d2dc9f0c55 100644 --- a/cli/src/tunnels/service_windows.rs +++ b/cli/src/tunnels/service_windows.rs @@ -78,6 +78,7 @@ impl CliServiceManager for WindowsService { cmd.stderr(Stdio::null()); cmd.stdout(Stdio::null()); cmd.stdin(Stdio::null()); + cmd.creation_flags(CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS); cmd.spawn() .map_err(|e| wrapdbg(e, "error starting service"))?; @@ -121,8 +122,12 @@ impl CliServiceManager for WindowsService { async fn unregister(&self) -> Result<(), AnyError> { let key = WindowsService::open_key()?; - key.delete_value(TUNNEL_ACTIVITY_NAME) - .map_err(|e| AnyError::from(wrap(e, "error deleting registry key")))?; + match key.delete_value(TUNNEL_ACTIVITY_NAME) { + Ok(_) => {} + Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} + Err(e) => return Err(wrap(e, "error deleting registry key").into()), + } + info!(self.log, "Tunnel service uninstalled"); let r = do_single_rpc_call::<_, ()>( diff --git a/src/vs/base/common/buffer.ts b/src/vs/base/common/buffer.ts index ff61eb5c9e2..08736ab8c0b 100644 --- a/src/vs/base/common/buffer.ts +++ b/src/vs/base/common/buffer.ts @@ -172,53 +172,59 @@ export class VSBuffer { writeUInt8(this.buffer, value, offset); } - indexOf(subarray: VSBuffer | Uint8Array) { - const needle = subarray instanceof VSBuffer ? subarray.buffer : subarray; - const needleLen = needle.byteLength; - const haystack = this.buffer; - const haystackLen = haystack.byteLength; - - if (needleLen === 0) { - return 0; - } - - if (needleLen === 1) { - return haystack.indexOf(needle[0]); - } - - if (needleLen > haystackLen) { - return -1; - } - - // find index of the subarray using boyer-moore-horspool algorithm - const table = indexOfTable.value; - table.fill(needle.length); - for (let i = 0; i < needle.length; i++) { - table[needle[i]] = needle.length - i - 1; - } - - let i = needle.length - 1; - let j = i; - let result = -1; - while (i < haystackLen) { - if (haystack[i] === needle[j]) { - if (j === 0) { - result = i; - break; - } - - i--; - j--; - } else { - i += Math.max(needle.length - j, table[haystack[i]]); - j = needle.length - 1; - } - } - - return result; + indexOf(subarray: VSBuffer | Uint8Array, offset = 0) { + return binaryIndexOf(this.buffer, subarray instanceof VSBuffer ? subarray.buffer : subarray, offset); } } +/** + * Like String.indexOf, but works on Uint8Arrays. + * Uses the boyer-moore-horspool algorithm to be reasonably speedy. + */ +export function binaryIndexOf(haystack: Uint8Array, needle: Uint8Array, offset = 0): number { + const needleLen = needle.byteLength; + const haystackLen = haystack.byteLength; + + if (needleLen === 0) { + return 0; + } + + if (needleLen === 1) { + return haystack.indexOf(needle[0]); + } + + if (needleLen > haystackLen - offset) { + return -1; + } + + // find index of the subarray using boyer-moore-horspool algorithm + const table = indexOfTable.value; + table.fill(needle.length); + for (let i = 0; i < needle.length; i++) { + table[needle[i]] = needle.length - i - 1; + } + + let i = offset + needle.length - 1; + let j = i; + let result = -1; + while (i < haystackLen) { + if (haystack[i] === needle[j]) { + if (j === 0) { + result = i; + break; + } + + i--; + j--; + } else { + i += Math.max(needle.length - j, table[haystack[i]]); + j = needle.length - 1; + } + } + + return result; +} + export function readUInt16LE(source: Uint8Array, offset: number): number { return ( ((source[offset + 0] << 0) >>> 0) | diff --git a/src/vs/base/node/nodeStreams.ts b/src/vs/base/node/nodeStreams.ts new file mode 100644 index 00000000000..0719bb46787 --- /dev/null +++ b/src/vs/base/node/nodeStreams.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Transform } from 'stream'; +import { binaryIndexOf } from 'vs/base/common/buffer'; + +/** + * A Transform stream that splits the input on the "splitter" substring. + * The resulting chunks will contain (and trail with) the splitter match. + * The last chunk when the stream ends will be emitted even if a splitter + * is not encountered. + */ +export class StreamSplitter extends Transform { + private buffer: Buffer | undefined; + private readonly splitter: Buffer | number; + private readonly spitterLen: number; + + constructor(splitter: string | number | Buffer) { + super(); + if (typeof splitter === 'number') { + this.splitter = splitter; + this.spitterLen = 1; + } else { + const buf = Buffer.isBuffer(splitter) ? splitter : Buffer.from(splitter); + this.splitter = buf.length === 1 ? buf[0] : buf; + this.spitterLen = buf.length; + } + } + + override _transform(chunk: Buffer, _encoding: string, callback: (error?: Error | null, data?: any) => void): void { + if (!this.buffer) { + this.buffer = chunk; + } else { + this.buffer = Buffer.concat([this.buffer, chunk]); + } + + let offset = 0; + while (offset < this.buffer.length) { + const index = typeof this.splitter === 'number' + ? this.buffer.indexOf(this.splitter, offset) + : binaryIndexOf(this.buffer, this.splitter, offset); + if (index === -1) { + break; + } + + this.push(this.buffer.slice(offset, index + this.spitterLen)); + offset = index + this.spitterLen; + } + + this.buffer = offset === this.buffer.length ? undefined : this.buffer.slice(offset); + callback(); + } + + override _flush(callback: (error?: Error | null, data?: any) => void): void { + if (this.buffer) { + this.push(this.buffer); + } + + callback(); + } +} diff --git a/src/vs/base/test/node/nodeStreams.test.ts b/src/vs/base/test/node/nodeStreams.test.ts new file mode 100644 index 00000000000..620f817dfc9 --- /dev/null +++ b/src/vs/base/test/node/nodeStreams.test.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { Writable } from 'stream'; +import * as assert from 'assert'; +import { StreamSplitter } from 'vs/base/node/nodeStreams'; + +suite('StreamSplitter', () => { + test('should split a stream on a single character splitter', (done) => { + const chunks: string[] = []; + const splitter = new StreamSplitter('\n'); + const writable = new Writable({ + write(chunk, _encoding, callback) { + chunks.push(chunk.toString()); + callback(); + }, + }); + + splitter.pipe(writable); + splitter.write('hello\nwor'); + splitter.write('ld\n'); + splitter.write('foo\nbar\nz'); + splitter.end(() => { + assert.deepStrictEqual(chunks, ['hello\n', 'world\n', 'foo\n', 'bar\n', 'z']); + done(); + }); + }); + + test('should split a stream on a multi-character splitter', (done) => { + const chunks: string[] = []; + const splitter = new StreamSplitter('---'); + const writable = new Writable({ + write(chunk, _encoding, callback) { + chunks.push(chunk.toString()); + callback(); + }, + }); + + splitter.pipe(writable); + splitter.write('hello---wor'); + splitter.write('ld---'); + splitter.write('foo---bar---z'); + splitter.end(() => { + assert.deepStrictEqual(chunks, ['hello---', 'world---', 'foo---', 'bar---', 'z']); + done(); + }); + }); +}); diff --git a/src/vs/platform/remoteTunnel/common/remoteTunnel.ts b/src/vs/platform/remoteTunnel/common/remoteTunnel.ts index 61654659195..d50077ee508 100644 --- a/src/vs/platform/remoteTunnel/common/remoteTunnel.ts +++ b/src/vs/platform/remoteTunnel/common/remoteTunnel.ts @@ -21,18 +21,33 @@ export interface IRemoteTunnelService { readonly onDidChangeTunnelStatus: Event; getTunnelStatus(): Promise; - getSession(): Promise; - readonly onDidChangeSession: Event; + getMode(): Promise; + readonly onDidChangeMode: Event; readonly onDidTokenFailed: Event; - initialize(session: IRemoteTunnelSession | undefined): Promise; + initialize(mode: TunnelMode): Promise; - startTunnel(session: IRemoteTunnelSession): Promise; + startTunnel(mode: ActiveTunnelMode): Promise; stopTunnel(): Promise; getTunnelName(): Promise; } +export interface ActiveTunnelMode { + readonly active: true; + readonly session: IRemoteTunnelSession; + readonly asService: boolean; +} + +export interface InactiveTunnelMode { + readonly active: false; +} + +export const INACTIVE_TUNNEL_MODE: InactiveTunnelMode = { active: false }; + +/** Saved mode for the tunnel. */ +export type TunnelMode = ActiveTunnelMode | InactiveTunnelMode; + export type TunnelStatus = TunnelStates.Connected | TunnelStates.Disconnected | TunnelStates.Connecting | TunnelStates.Uninitialized; export namespace TunnelStates { @@ -46,13 +61,14 @@ export namespace TunnelStates { export interface Connected { readonly type: 'connected'; readonly info: ConnectionInfo; + readonly serviceInstallFailed: boolean; } export interface Disconnected { readonly type: 'disconnected'; readonly onTokenFailed?: IRemoteTunnelSession; } export const disconnected = (onTokenFailed?: IRemoteTunnelSession): Disconnected => ({ type: 'disconnected', onTokenFailed }); - export const connected = (info: ConnectionInfo): Connected => ({ type: 'connected', info }); + export const connected = (info: ConnectionInfo, serviceInstallFailed: boolean): Connected => ({ type: 'connected', info, serviceInstallFailed }); export const connecting = (progress?: string): Connecting => ({ type: 'connecting', progress }); export const uninitialized: Uninitialized = { type: 'uninitialized' }; diff --git a/src/vs/platform/remoteTunnel/node/remoteTunnelService.ts b/src/vs/platform/remoteTunnel/node/remoteTunnelService.ts index 84ccef33653..08ba634bad0 100644 --- a/src/vs/platform/remoteTunnel/node/remoteTunnelService.ts +++ b/src/vs/platform/remoteTunnel/node/remoteTunnelService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CONFIGURATION_KEY_HOST_NAME, CONFIGURATION_KEY_PREVENT_SLEEP, ConnectionInfo, IRemoteTunnelSession, IRemoteTunnelService, LOGGER_NAME, LOG_ID, TunnelStates, TunnelStatus } from 'vs/platform/remoteTunnel/common/remoteTunnel'; +import { CONFIGURATION_KEY_HOST_NAME, CONFIGURATION_KEY_PREVENT_SLEEP, ConnectionInfo, IRemoteTunnelSession, IRemoteTunnelService, LOGGER_NAME, LOG_ID, TunnelStates, TunnelStatus, TunnelMode, INACTIVE_TUNNEL_MODE, ActiveTunnelMode } from 'vs/platform/remoteTunnel/common/remoteTunnel'; import { Emitter } from 'vs/base/common/event'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -20,15 +20,18 @@ import { localize } from 'vs/nls'; import { hostname, homedir } from 'os'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { isString } from 'vs/base/common/types'; +import { StreamSplitter } from 'vs/base/node/nodeStreams'; type RemoteTunnelEnablementClassification = { owner: 'aeschli'; comment: 'Reporting when Remote Tunnel access is turned on or off'; enabled?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Flag indicating if Remote Tunnel Access is enabled or not' }; + service?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Flag indicating if Remote Tunnel Access is installed as a service' }; }; type RemoteTunnelEnablementEvent = { enabled: boolean; + service: boolean; }; const restartTunnelOnConfigurationChanges: readonly string[] = [ @@ -40,6 +43,8 @@ const restartTunnelOnConfigurationChanges: readonly string[] = [ // if set, the remote tunnel access is currently enabled. // if not set, the remote tunnel access is currently disabled. const TUNNEL_ACCESS_SESSION = 'remoteTunnelSession'; +// Boolean indicating whether the tunnel should be installed as a service. +const TUNNEL_ACCESS_IS_SERVICE = 'remoteTunnelIsService'; /** * This service runs on the shared service. It is running the `code-tunnel` command @@ -55,12 +60,19 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ private readonly _onDidChangeTunnelStatusEmitter = new Emitter(); public readonly onDidChangeTunnelStatus = this._onDidChangeTunnelStatusEmitter.event; - private readonly _onDidChangeSessionEmitter = new Emitter(); - public readonly onDidChangeSession = this._onDidChangeSessionEmitter.event; + private readonly _onDidChangeModeEmitter = new Emitter(); + public readonly onDidChangeMode = this._onDidChangeModeEmitter.event; private readonly _logger: ILogger; - private _session: IRemoteTunnelSession | undefined; + /** + * "Mode" in the terminal state we want to get to -- started, stopped, and + * the attributes associated with each. + * + * At any given time, work may be ongoing to get `_tunnelStatus` into a + * state that reflects the desired `mode`. + */ + private _mode: TunnelMode = INACTIVE_TUNNEL_MODE; private _tunnelProcess: CancelablePromise | undefined; @@ -98,7 +110,7 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ } })); - this._session = this._restoreSession(); + this._mode = this._restoreMode(); this._tunnelStatus = TunnelStates.uninitialized; } @@ -111,32 +123,34 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ this._onDidChangeTunnelStatusEmitter.fire(tunnelStatus); } - private setSession(session: IRemoteTunnelSession | undefined) { - if (!isSameSession(session, this._session)) { - this._session = session; - this._onDidChangeSessionEmitter.fire(session); - this._storeSession(session); - if (session) { - this._logger.info(`Session updated: ${session.accountLabel} (${session.providerId})`); - if (session.token) { - this._logger.info(`Session token updated: ${session.accountLabel} (${session.providerId})`); - } - } else { - this._logger.info(`Session reset`); + private setMode(mode: TunnelMode) { + if (isSameMode(this._mode, mode)) { + return; + } + + this._mode = mode; + this._storeMode(mode); + this._onDidChangeModeEmitter.fire(this._mode); + if (mode.active) { + this._logger.info(`Session updated: ${mode.session.accountLabel} (${mode.session.providerId}) (service=${mode.asService})`); + if (mode.session.token) { + this._logger.info(`Session token updated: ${mode.session.accountLabel} (${mode.session.providerId})`); } + } else { + this._logger.info(`Session reset`); } } - async getSession(): Promise { - return this._session; + getMode(): Promise { + return Promise.resolve(this._mode); } - async initialize(session: IRemoteTunnelSession | undefined): Promise { + async initialize(mode: TunnelMode): Promise { if (this._initialized) { return this._tunnelStatus; } this._initialized = true; - this.setSession(session); + this.setMode(mode); try { await this._startTunnelProcessDelayer.trigger(() => this.updateTunnelProcess()); } catch (e) { @@ -145,6 +159,14 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ return this._tunnelStatus; } + private readonly defaultOnOutput = (a: string, isErr: boolean) => { + if (isErr) { + this._logger.error(a); + } else { + this._logger.info(a); + } + }; + private getTunnelCommandLocation() { if (!this._tunnelCommand) { let binParentLocation; @@ -164,11 +186,12 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ return this._tunnelCommand; } - async startTunnel(session: IRemoteTunnelSession): Promise { - if (isSameSession(session, this._session) && this._tunnelStatus.type !== 'disconnected') { + async startTunnel(mode: ActiveTunnelMode): Promise { + if (isSameMode(this._mode, mode) && this._tunnelStatus.type !== 'disconnected') { return this._tunnelStatus; } - this.setSession(session); + + this.setMode(mode); try { await this._startTunnelProcessDelayer.trigger(() => this.updateTunnelProcess()); @@ -180,41 +203,49 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ async stopTunnel(): Promise { - this.setSession(undefined); - if (this._tunnelProcess) { this._tunnelProcess.cancel(); this._tunnelProcess = undefined; } - const onOutput = (a: string, isErr: boolean) => { - if (isErr) { - this._logger.error(a); - } else { - this._logger.info(a); - } - }; + if (!this._mode.active) { + return; + } + + // Be careful to only uninstall the service if we're the ones who installed it: + const needsServiceUninstall = this._mode.asService; + this.setMode(INACTIVE_TUNNEL_MODE); + try { - await this.runCodeTunnelCommand('stop', ['kill'], onOutput); + if (needsServiceUninstall) { + this.runCodeTunnelCommand('uninstallService', ['service', 'uninstall']); + } } catch (e) { this._logger.error(e); } - this.setTunnelStatus(TunnelStates.disconnected()); + try { + await this.runCodeTunnelCommand('stop', ['kill']); + } catch (e) { + this._logger.error(e); + } + + this.setTunnelStatus(TunnelStates.disconnected()); } private async updateTunnelProcess(): Promise { - this.telemetryService.publicLog2('remoteTunnel.enablement', { enabled: !!this._session }); - + this.telemetryService.publicLog2('remoteTunnel.enablement', { + enabled: this._mode.active, + service: this._mode.active && this._mode.asService, + }); if (this._tunnelProcess) { this._tunnelProcess.cancel(); this._tunnelProcess = undefined; } - let isAttached = false; let output = ''; - + let isServiceInstalled = false; const onOutput = (a: string, isErr: boolean) => { if (isErr) { this._logger.error(a); @@ -241,22 +272,26 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ tunnel: object | null; } = JSON.parse(output.trim().split('\n').find(l => l.startsWith('{'))!); - isAttached = !!status.tunnel; - this._logger.info(isAttached ? 'Other tunnel running, attaching...' : 'No other tunnel running'); - if (!isAttached && !this._session) { - this._tunnelProcess = undefined; + isServiceInstalled = status.service_installed; + this._logger.info(status.tunnel ? 'Other tunnel running, attaching...' : 'No other tunnel running'); + + // If a tunnel is running but the mode isn't "active", we'll still attach + // to the tunnel to show its state in the UI. If neither are true, disconnect + if (!status.tunnel && !this._mode.active) { this.setTunnelStatus(TunnelStates.disconnected()); return; } } catch (e) { this._logger.error(e); - this._tunnelProcess = undefined; this.setTunnelStatus(TunnelStates.disconnected()); return; + } finally { + if (this._tunnelProcess === statusProcess) { + this._tunnelProcess = undefined; + } } - const session = this._session; - + const session = this._mode.active ? this._mode.session : undefined; if (session && session.token) { const token = session.token; this.setTunnelStatus(TunnelStates.connecting(localize({ key: 'remoteTunnelService.authorizing', comment: ['{0} is a user account name, {1} a provider name (e.g. Github)'] }, 'Connecting as {0} ({1})', session.accountLabel, session.providerId))); @@ -286,25 +321,67 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ } else { this.setTunnelStatus(TunnelStates.connecting(localize('remoteTunnelService.openTunnel', 'Opening tunnel'))); } - const args = ['--parent-process-id', String(process.pid), '--accept-server-license-terms', '--log', LogLevelToString(this._logger.getLevel())]; + const args = ['--accept-server-license-terms', '--log', LogLevelToString(this._logger.getLevel())]; if (hostName) { args.push('--name', hostName); } else { args.push('--random-name'); } + + let serviceInstallFailed = false; + if (this._mode.active && this._mode.asService && !isServiceInstalled) { + // I thought about calling `code tunnel kill` here, but having multiple + // tunnel processes running is pretty much idempotent. If there's + // another tunnel process running, the service process will + // take over when it exits, no hard feelings. + serviceInstallFailed = await this.installTunnelService(args) === false; + } + + return this.serverOrAttachTunnel(session, args, serviceInstallFailed); + } + + private async installTunnelService(args: readonly string[]) { + let status: number; + try { + status = await this.runCodeTunnelCommand('serviceInstall', ['service', 'install', ...args]); + } catch (e) { + this._logger.error(e); + status = 1; + } + + if (status !== 0) { + const msg = localize('remoteTunnelService.serviceInstallFailed', 'Failed to install tunnel as a service, starting in session...'); + this._logger.warn(msg); + this.setTunnelStatus(TunnelStates.connecting(msg)); + return false; + } + + return true; + } + + private async serverOrAttachTunnel(session: IRemoteTunnelSession | undefined, args: string[], serviceInstallFailed: boolean) { + args.push('--parent-process-id', String(process.pid)); + if (this._preventSleep()) { args.push('--no-sleep'); } + + let isAttached = false; const serveCommand = this.runCodeTunnelCommand('tunnel', args, (message: string, isErr: boolean) => { if (isErr) { this._logger.error(message); } else { this._logger.info(message); } + + if (message.includes('Connected to an existing tunnel process')) { + isAttached = true; + } + const m = message.match(/Open this link in your browser (https:\/\/([^\/\s]+)\/([^\/\s]+)\/([^\/\s]+))/); if (m) { const info: ConnectionInfo = { link: m[1], domain: m[2], tunnelName: m[4], isAttached }; - this.setTunnelStatus(TunnelStates.connected(info)); + this.setTunnelStatus(TunnelStates.connected(info, serviceInstallFailed)); } else if (message.match(/error refreshing token/)) { serveCommand.cancel(); this._onDidTokenFailedEmitter.fire(session); @@ -317,14 +394,14 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ // process exited unexpectedly this._logger.info(`tunnel process terminated`); this._tunnelProcess = undefined; - this._session = undefined; + this._mode = INACTIVE_TUNNEL_MODE; this.setTunnelStatus(TunnelStates.disconnected()); } }); } - private runCodeTunnelCommand(logLabel: string, commandArgs: string[], onOutput: (message: string, isError: boolean) => void = () => { }): CancelablePromise { + private runCodeTunnelCommand(logLabel: string, commandArgs: string[], onOutput: (message: string, isError: boolean) => void = this.defaultOnOutput): CancelablePromise { return createCancelablePromise(token => { return new Promise((resolve, reject) => { if (token.isCancellationRequested) { @@ -350,13 +427,13 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ tunnelProcess = spawn(tunnelCommand, ['tunnel', ...commandArgs], { cwd: homedir(), stdio }); } - tunnelProcess.stdout!.on('data', data => { + tunnelProcess.stdout!.pipe(new StreamSplitter('\n')).on('data', data => { if (tunnelProcess) { const message = data.toString(); onOutput(message, false); } }); - tunnelProcess.stderr!.on('data', data => { + tunnelProcess.stderr!.pipe(new StreamSplitter('\n')).on('data', data => { if (tunnelProcess) { const message = data.toString(); onOutput(message, true); @@ -394,30 +471,33 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ return name || undefined; } - private _restoreSession(): IRemoteTunnelSession | undefined { + private _restoreMode(): TunnelMode { try { const tunnelAccessSession = this.storageService.get(TUNNEL_ACCESS_SESSION, StorageScope.APPLICATION); + const asService = this.storageService.getBoolean(TUNNEL_ACCESS_IS_SERVICE, StorageScope.APPLICATION, false); if (tunnelAccessSession) { const session = JSON.parse(tunnelAccessSession) as IRemoteTunnelSession; if (session && isString(session.accountLabel) && isString(session.sessionId) && isString(session.providerId)) { - return session; + return { active: true, session, asService }; } this._logger.error('Problems restoring session from storage, invalid format', session); } } catch (e) { this._logger.error('Problems restoring session from storage', e); } - return undefined; + return INACTIVE_TUNNEL_MODE; } - private _storeSession(session: IRemoteTunnelSession | undefined): void { - if (session) { + private _storeMode(mode: TunnelMode): void { + if (mode.active) { const sessionWithoutToken = { - providerId: session.providerId, sessionId: session.sessionId, accountLabel: session.accountLabel + providerId: mode.session.providerId, sessionId: mode.session.sessionId, accountLabel: mode.session.accountLabel }; this.storageService.store(TUNNEL_ACCESS_SESSION, JSON.stringify(sessionWithoutToken), StorageScope.APPLICATION, StorageTarget.MACHINE); + this.storageService.store(TUNNEL_ACCESS_IS_SERVICE, mode.asService, StorageScope.APPLICATION, StorageTarget.MACHINE); } else { this.storageService.remove(TUNNEL_ACCESS_SESSION, StorageScope.APPLICATION); + this.storageService.remove(TUNNEL_ACCESS_IS_SERVICE, StorageScope.APPLICATION); } } } @@ -429,3 +509,12 @@ function isSameSession(a1: IRemoteTunnelSession | undefined, a2: IRemoteTunnelSe return a1 === a2; } +const isSameMode = (a: TunnelMode, b: TunnelMode) => { + if (a.active !== b.active) { + return false; + } else if (a.active && b.active) { + return a.asService === b.asService && isSameSession(a.session, b.session); + } else { + return true; + } +}; diff --git a/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts b/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts index 7c6b0fe8cf5..5885e400570 100644 --- a/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts +++ b/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts @@ -3,40 +3,39 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; -import { IProductService } from 'vs/platform/product/common/productService'; -import { CONFIGURATION_KEY_HOST_NAME, CONFIGURATION_KEY_PREFIX, CONFIGURATION_KEY_PREVENT_SLEEP, ConnectionInfo, IRemoteTunnelSession, IRemoteTunnelService, LOGGER_NAME, LOG_ID } from 'vs/platform/remoteTunnel/common/remoteTunnel'; -import { AuthenticationSession, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; -import { localize } from 'vs/nls'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { ILocalizedString } from 'vs/platform/action/common/action'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { ILogger, ILoggerService, ILogService } from 'vs/platform/log/common/log'; -import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IQuickInputService, IQuickPickItem, IQuickPickSeparator, QuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { IOutputService } from 'vs/workbench/services/output/common/output'; -import { IFileService } from 'vs/platform/files/common/files'; -import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; -import { IProgress, IProgressService, IProgressStep, ProgressLocation } from 'vs/platform/progress/common/progress'; -import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; import { Action } from 'vs/base/common/actions'; -import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { IWorkspaceContextService, isUntitledWorkspace } from 'vs/platform/workspace/common/workspace'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; -import { URI } from 'vs/base/common/uri'; -import { joinPath } from 'vs/base/common/resources'; import { ITunnelApplicationConfig } from 'vs/base/common/product'; +import { joinPath } from 'vs/base/common/resources'; import { isNumber, isObject, isString } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { ILocalizedString } from 'vs/platform/action/common/action'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ILogger, ILoggerService } from 'vs/platform/log/common/log'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IProgress, IProgressService, IProgressStep, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { IQuickInputService, IQuickPickItem, IQuickPickSeparator, QuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { CONFIGURATION_KEY_HOST_NAME, CONFIGURATION_KEY_PREFIX, CONFIGURATION_KEY_PREVENT_SLEEP, ConnectionInfo, INACTIVE_TUNNEL_MODE, IRemoteTunnelService, IRemoteTunnelSession, LOGGER_NAME, LOG_ID, TunnelStatus } from 'vs/platform/remoteTunnel/common/remoteTunnel'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IWorkspaceContextService, isUntitledWorkspace } from 'vs/platform/workspace/common/workspace'; +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { AuthenticationSession, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IOutputService } from 'vs/workbench/services/output/common/output'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; export const REMOTE_TUNNEL_CATEGORY: ILocalizedString = { original: 'Remote-Tunnels', @@ -103,10 +102,8 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo @IProductService productService: IProductService, @IStorageService private readonly storageService: IStorageService, @ILoggerService loggerService: ILoggerService, - @ILogService logService: ILogService, @IQuickInputService private readonly quickInputService: IQuickInputService, @INativeEnvironmentService private environmentService: INativeEnvironmentService, - @IFileService fileService: IFileService, @IRemoteTunnelService private remoteTunnelService: IRemoteTunnelService, @ICommandService private commandService: ICommandService, @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, @@ -127,20 +124,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo } this.serverConfiguration = serverConfiguration; - this._register(this.remoteTunnelService.onDidChangeTunnelStatus(status => { - this.connectionInfo = undefined; - if (status.type === 'disconnected') { - if (status.onTokenFailed) { - this.expiredSessions.add(status.onTokenFailed.sessionId); - } - this.connectionStateContext.set('disconnected'); - } else if (status.type === 'connecting') { - this.connectionStateContext.set('connecting'); - } else if (status.type === 'connected') { - this.connectionInfo = status.info; - this.connectionStateContext.set('connected'); - } - })); + this._register(this.remoteTunnelService.onDidChangeTunnelStatus(s => this.handleTunnelStatusUpdate(s))); this.registerCommands(); @@ -149,6 +133,21 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo this.recommendRemoteExtensionIfNeeded(); } + private handleTunnelStatusUpdate(status: TunnelStatus) { + this.connectionInfo = undefined; + if (status.type === 'disconnected') { + if (status.onTokenFailed) { + this.expiredSessions.add(status.onTokenFailed.sessionId); + } + this.connectionStateContext.set('disconnected'); + } else if (status.type === 'connecting') { + this.connectionStateContext.set('connecting'); + } else if (status.type === 'connected') { + this.connectionInfo = status.info; + this.connectionStateContext.set('connected'); + } + } + private async recommendRemoteExtensionIfNeeded() { await this.extensionService.whenInstalledExtensionsRegistered(); @@ -228,10 +227,17 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo } private async initialize(): Promise { - const session = await this.remoteTunnelService.getSession(); - if (session && session.token) { + const [mode, status] = await Promise.all([ + this.remoteTunnelService.getMode(), + this.remoteTunnelService.getTunnelStatus(), + ]); + + this.handleTunnelStatusUpdate(status); + + if (mode.active && mode.session.token) { return; // already initialized, token available } + return await this.progressService.withProgress( { location: ProgressLocation.Window, @@ -248,13 +254,13 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo } }); let newSession: IRemoteTunnelSession | undefined; - if (session) { - const token = await this.getSessionToken(session); + if (mode.active) { + const token = await this.getSessionToken(mode.session); if (token) { - newSession = { ...session, token }; + newSession = { ...mode.session, token }; } } - const status = await this.remoteTunnelService.initialize(newSession); + const status = await this.remoteTunnelService.initialize(mode.active && newSession ? { ...mode, session: newSession } : INACTIVE_TUNNEL_MODE); listener.dispose(); if (status.type === 'connected') { @@ -267,7 +273,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo } - private async startTunnel(): Promise { + private async startTunnel(asService: boolean): Promise { if (this.connectionInfo) { return this.connectionInfo; } @@ -301,6 +307,19 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo listener.dispose(); completed = true; s(status.info); + if (status.serviceInstallFailed) { + this.notificationService.notify({ + severity: Severity.Warning, + message: localize( + { + key: 'remoteTunnel.serviceInstallFailed', + comment: ['{Locked="](command:{0})"}'] + }, + "Installation as a service failed, and we fell back to running the tunnel for this session. See the [error log](command:{0}) for details.", + RemoteTunnelCommandIds.showLog, + ), + }); + } break; case 'disconnected': listener.dispose(); @@ -312,7 +331,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo }); const token = authenticationSession.session.idToken ?? authenticationSession.session.accessToken; const account: IRemoteTunnelSession = { sessionId: authenticationSession.session.id, token, providerId: authenticationSession.providerId, accountLabel: authenticationSession.session.account.label }; - this.remoteTunnelService.startTunnel(account).then(status => { + this.remoteTunnelService.startTunnel({ active: true, asService, session: account }).then(status => { if (!completed && (status.type === 'connected' || status.type === 'disconnected')) { listener.dispose(); if (status.type === 'connected') { @@ -403,7 +422,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo private async getAllSessions(): Promise { const authenticationProviders = await this.getAuthenticationProviders(); const accounts = new Map(); - const currentAccount = await this.remoteTunnelService.getSession(); + const currentAccount = await this.remoteTunnelService.getMode(); let currentSession: ExistingSessionItem | undefined; for (const provider of authenticationProviders) { @@ -413,7 +432,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo if (!this.expiredSessions.has(session.id)) { const item = this.createExistingSessionItem(session, provider.id); accounts.set(item.session.account.id, item); - if (currentAccount && currentAccount.sessionId === session.id) { + if (currentAccount.active && currentAccount.session.sessionId === session.id) { currentSession = item; } } @@ -483,6 +502,8 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo const commandService = accessor.get(ICommandService); const storageService = accessor.get(IStorageService); const dialogService = accessor.get(IDialogService); + const quickInputService = accessor.get(IQuickInputService); + const productService = accessor.get(IProductService); const didNotifyPreview = storageService.getBoolean(REMOTE_TUNNEL_PROMPTED_PREVIEW_STORAGE_KEY, StorageScope.APPLICATION, false); if (!didNotifyPreview) { @@ -497,12 +518,33 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo storageService.store(REMOTE_TUNNEL_PROMPTED_PREVIEW_STORAGE_KEY, true, StorageScope.APPLICATION, StorageTarget.USER); } - const connectionInfo = await that.startTunnel(); + const disposables = new DisposableStore(); + const quickPick = quickInputService.createQuickPick(); + quickPick.placeholder = localize('tunnel.enable.placeholder', 'Select how you want to enable access'); + quickPick.items = [ + { service: false, label: localize('tunnel.enable.session', 'Turn on for this session'), description: localize('tunnel.enable.session.description', 'Run whenever {0} is open', productService.nameShort) }, + { service: true, label: localize('tunnel.enable.service', 'Install as a service'), description: localize('tunnel.enable.service.description', 'Run whenever you\'re logged in') } + ]; + + const asService = await new Promise(resolve => { + disposables.add(quickPick.onDidAccept(() => resolve(quickPick.selectedItems[0]?.service))); + disposables.add(quickPick.onDidHide(() => resolve(undefined))); + quickPick.show(); + }); + + quickPick.dispose(); + + if (asService === undefined) { + return; // no-op + } + + const connectionInfo = await that.startTunnel(/* installAsService= */ asService); + if (connectionInfo) { const linkToOpen = that.getLinkToOpen(connectionInfo); const remoteExtension = that.serverConfiguration.extension; const linkToOpenForMarkdown = linkToOpen.toString(false).replace(/\)/g, '%29'); - await notificationService.notify({ + notificationService.notify({ severity: Severity.Info, message: localize( @@ -525,7 +567,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo const usedOnHostMessage: UsedOnHostMessage = { hostName: connectionInfo.tunnelName, timeStamp: new Date().getTime() }; storageService.store(REMOTE_TUNNEL_USED_STORAGE_KEY, JSON.stringify(usedOnHostMessage), StorageScope.APPLICATION, StorageTarget.USER); } else { - await notificationService.notify({ + notificationService.notify({ severity: Severity.Info, message: localize('progress.turnOn.failed', "Unable to turn on the remote tunnel access. Check the Remote Tunnel Service log for details."), @@ -699,7 +741,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo private async showManageOptions() { - const account = await this.remoteTunnelService.getSession(); + const account = await this.remoteTunnelService.getMode(); return new Promise((c, e) => { const disposables = new DisposableStore(); @@ -721,7 +763,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo items.push({ id: RemoteTunnelCommandIds.showLog, label: localize('manage.showLog', 'Show Log') }); items.push({ type: 'separator' }); items.push({ id: RemoteTunnelCommandIds.configure, label: localize('manage.tunnelName', 'Change Tunnel Name'), description: this.connectionInfo?.tunnelName }); - items.push({ id: RemoteTunnelCommandIds.turnOff, label: RemoteTunnelCommandLabels.turnOff, description: account ? `${account.accountLabel} (${account.providerId})` : undefined }); + items.push({ id: RemoteTunnelCommandIds.turnOff, label: RemoteTunnelCommandLabels.turnOff, description: account.active ? `${account.session.accountLabel} (${account.session.providerId})` : undefined }); quickPick.items = items; disposables.add(quickPick.onDidAccept(() => {