diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index bd24587d0a7..f8bb0950acf 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -119,7 +119,7 @@ class SharedProcessMain extends Disposable { private sharedProcessWorkerService: ISharedProcessWorkerService | undefined = undefined; - private lifecycleService: SharedProcessLifecycleService | undefined = undefined;; + private lifecycleService: SharedProcessLifecycleService | undefined = undefined; constructor(private configuration: ISharedProcessConfiguration) { super(); diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index 297b2cf012e..80457bc319a 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -57,7 +57,7 @@ export async function main(argv: string[]): Promise { return new Promise((resolve, reject) => { let tunnelProcess; if (process.env['VSCODE_DEV']) { - tunnelProcess = spawn('cargo', ['run', '--bin', 'code-tunnel', ...argv.slice(5)], { cwd: join(getAppRoot(), 'cli') }); + tunnelProcess = spawn('cargo', ['run', '--bin', 'code-tunnel', '--', ...argv.slice(5)], { cwd: join(getAppRoot(), 'cli') }); } else { const tunnelCommand = join(dirname(process.execPath), 'bin', `${product.tunnelApplicationName}${isWindows ? '.exe' : ''}`); const tunnelArgs = argv.slice(3); diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 9ea98d94eb4..ec934e807eb 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -38,9 +38,9 @@ export interface Subcommand { export type OptionDescriptions = { [P in keyof T]: - T[P] extends boolean ? Option<'boolean'> : - T[P] extends string ? Option<'string'> : - T[P] extends string[] ? Option<'string[]'> : + T[P] extends boolean | undefined ? Option<'boolean'> : + T[P] extends string | undefined ? Option<'string'> : + T[P] extends string[] | undefined ? Option<'string[]'> : Subcommand }; @@ -249,7 +249,8 @@ export function parseArgs(args: string[], options: OptionDescriptions, err const reporter = errorReporter.getSubcommandReporter ? errorReporter.getSubcommandReporter(firstArg) : undefined; const subcommandOptions = parseArgs(newArgs, options, reporter); return { - [firstArg]: subcommandOptions + [firstArg]: subcommandOptions, + _: [] }; } diff --git a/src/vs/platform/environment/test/node/argv.test.ts b/src/vs/platform/environment/test/node/argv.test.ts index fc182805893..b0d9a5661f4 100644 --- a/src/vs/platform/environment/test/node/argv.test.ts +++ b/src/vs/platform/environment/test/node/argv.test.ts @@ -103,33 +103,56 @@ suite('parseArgs', () => { } test('subcommands', () => { - const options1 = { + + interface TestArgs1 { + testcmd?: { + testArg?: string; + _: string[]; + }; + _: string[]; + } + + const options1: OptionDescriptions = { 'testcmd': c('A test command', { - testArg: o('A test command option') - }) + testArg: o('A test command option'), + _: { type: 'string[]' } + }), + _: { type: 'string[]' } }; assertParse( options1, ['testcmd', '--testArg=foo'], - { testcmd: { testArg: 'foo', '_': [] } }, + { testcmd: { testArg: 'foo', '_': [] }, '_': [] }, [] ); assertParse( options1, ['testcmd', '--testArg=foo', '--testX'], - { testcmd: { testArg: 'foo', '_': [] } }, + { testcmd: { testArg: 'foo', '_': [] }, '_': [] }, ['testcmd-onUnknownOption testX'] ); - const options2 = { + + interface TestArgs2 { + testcmd?: { + testArg?: string; + testX?: boolean; + _: string[]; + }; + testX?: boolean; + _: string[]; + } + + const options2: OptionDescriptions = { 'testcmd': c('A test command', { testArg: o('A test command option') }), - testX: { global: true } + testX: { type: 'boolean', global: true, description: '' }, + _: { type: 'string[]' } }; assertParse( options2, ['testcmd', '--testArg=foo', '--testX'], - { testcmd: { testArg: 'foo', testX: true, '_': [] } }, + { testcmd: { testArg: 'foo', testX: true, '_': [] }, '_': [] }, [] ); }); diff --git a/src/vs/platform/remoteTunnel/common/remoteTunnel.ts b/src/vs/platform/remoteTunnel/common/remoteTunnel.ts index f7c9e2a3609..e114014cc12 100644 --- a/src/vs/platform/remoteTunnel/common/remoteTunnel.ts +++ b/src/vs/platform/remoteTunnel/common/remoteTunnel.ts @@ -31,3 +31,5 @@ export const enum TunnelStatus { Connecting = 'connecting', Connected = 'connected', } + +export const HOST_NAME_CONFIGURATION_KEY = 'remoteTunnel.hostNameOverride'; diff --git a/src/vs/platform/remoteTunnel/electron-browser/remoteTunnelService.ts b/src/vs/platform/remoteTunnel/electron-browser/remoteTunnelService.ts index 5ee3758c59d..e55155b3b99 100644 --- a/src/vs/platform/remoteTunnel/electron-browser/remoteTunnelService.ts +++ b/src/vs/platform/remoteTunnel/electron-browser/remoteTunnelService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IRemoteTunnelAccount, IRemoteTunnelService, TunnelStatus } from 'vs/platform/remoteTunnel/common/remoteTunnel'; +import { HOST_NAME_CONFIGURATION_KEY, IRemoteTunnelAccount, IRemoteTunnelService, TunnelStatus } 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'; @@ -14,10 +14,12 @@ import { dirname, join } from 'vs/base/common/path'; import { ChildProcess, spawn } from 'child_process'; import { IProductService } from 'vs/platform/product/common/productService'; import { isWindows } from 'vs/base/common/platform'; -import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; +import { CancelablePromise, createCancelablePromise, Delayer } from 'vs/base/common/async'; import { ISharedProcessLifecycleService } from 'vs/platform/lifecycle/electron-browser/sharedProcessLifecycleService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { hostname } from 'os'; + type RemoteTunnelEnablementClassification = { owner: 'aeschli'; @@ -52,6 +54,7 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ private _tunnelProcess: CancelablePromise | undefined; private _tunnelStatus: TunnelStatus = TunnelStatus.Disconnected; + private _startTunnelProcessDelayer: Delayer; constructor( @ITelemetryService private readonly telemetryService: ITelemetryService, @@ -59,11 +62,12 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ @INativeEnvironmentService private readonly environmentService: INativeEnvironmentService, @ILoggerService loggerService: ILoggerService, @ISharedProcessLifecycleService sharedProcessLifecycleService: ISharedProcessLifecycleService, - @IConfigurationService configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); const logFileUri = URI.file(join(dirname(environmentService.logsPath), 'remoteTunnel.log')); this._logger = this._register(loggerService.createLogger(logFileUri, { name: 'remoteTunnel' })); + this._startTunnelProcessDelayer = new Delayer(100); this._register(sharedProcessLifecycleService.onWillShutdown(e => { if (this._tunnelProcess) { @@ -72,6 +76,12 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ } this.dispose(); })); + + this._register(configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(HOST_NAME_CONFIGURATION_KEY)) { + this._startTunnelProcessDelayer.trigger(() => this.updateTunnelProcess()); + } + })); } async getAccount(): Promise { @@ -88,7 +98,7 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ this.telemetryService.publicLog2('remoteTunnel.enablement', { enabled: !!account }); try { - this.updateTunnelProcess(); + this._startTunnelProcessDelayer.trigger(() => this.updateTunnelProcess()); } catch (e) { this._logger.error(e); } @@ -110,28 +120,45 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ this._tunnelProcess = loginProcess; try { await loginProcess; + if (this._tunnelProcess !== loginProcess) { + return; + } } catch (e) { this._logger.error(e); this._tunnelProcess = undefined; this._onDidTokenFailedEmitter.fire(true); this.setTunnelStatus(TunnelStatus.Disconnected); + return; } - if (this._tunnelProcess === loginProcess) { - const serveCommand = this.runCodeTunneCommand('tunnel', ['--random-name'], (message: string) => { - }); - this._tunnelProcess = serveCommand; - serveCommand.finally(() => { - if (serveCommand === this._tunnelProcess) { - // process exited unexpectedly - this._logger.info(`tunnel process terminated`); - this._tunnelProcess = undefined; - this._account = undefined; - this.setTunnelStatus(TunnelStatus.Disconnected); + let hostName = this.getHostName(); + if (hostName) { + const setNameProcess = this.runCodeTunneCommand('set name', ['rename', hostName]); + this._tunnelProcess = setNameProcess; + try { + await setNameProcess; + if (this._tunnelProcess !== setNameProcess) { + return; } - }); - + } catch (e) { + this._logger.error(e); + hostName = undefined; + } } + const args = hostName ? [] : ['--random-name']; + const serveCommand = this.runCodeTunneCommand('tunnel', args, (message: string) => { + }); + this._tunnelProcess = serveCommand; + serveCommand.finally(() => { + if (serveCommand === this._tunnelProcess) { + // process exited unexpectedly + this._logger.info(`tunnel process terminated`); + this._tunnelProcess = undefined; + this._account = undefined; + + this.setTunnelStatus(TunnelStatus.Disconnected); + } + }); } private setTunnelStatus(tunnelStatus: TunnelStatus) { @@ -182,7 +209,11 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ if (tunnelProcess) { this._logger.info(`${logLabel} exit (${tunnelProcess.pid}): + ${e}`); tunnelProcess = undefined; - resolve(); + if (e === 0) { + resolve(); + } else { + reject(); + } } }); tunnelProcess.on('error', e => { @@ -196,4 +227,17 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ }); } + private getHostName() { + const name = this.configurationService.getValue(HOST_NAME_CONFIGURATION_KEY); + if (name && name.match(/^([\w-]+)$/) && name.length <= 20) { + return name; + } + const hostName = hostname(); + if (hostName && hostName.match(/^([\w-]+)$/)) { + return hostName; + } + + return undefined; + } + } 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 5572254d5e2..6193c22d7b3 100644 --- a/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts +++ b/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts @@ -6,7 +6,7 @@ import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IRemoteTunnelService, TunnelStatus } from 'vs/platform/remoteTunnel/common/remoteTunnel'; +import { HOST_NAME_CONFIGURATION_KEY, IRemoteTunnelService, TunnelStatus } from 'vs/platform/remoteTunnel/common/remoteTunnel'; import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; import { localize } from 'vs/nls'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -25,6 +25,7 @@ import { IStringDictionary } from 'vs/base/common/collections'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { registerLogChannel } from 'vs/workbench/services/output/common/output'; import { IFileService } from 'vs/platform/files/common/files'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; export const REMOTE_TUNNEL_CATEGORY: ILocalizedString = { original: 'Remote Tunnel', @@ -380,3 +381,17 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(RemoteTunnelWorkbenchContribution, LifecyclePhase.Restored); + +Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ + 'type': 'object', + 'properties': { + [HOST_NAME_CONFIGURATION_KEY]: { + 'description': localize('remoteTunnel.machineName', "The name under which the machine is remote tunnel is registered. If not set, the host name is used."), + 'type': 'string', + 'pattern': '[\\w-]+', + 'patternErrorMessage': localize('remoteTunnel.machineNameRegex', "The name can only consist of letters, numbers, underscore and minus."), + 'maxLength': 20, + 'default': '' + } + } +});