From 0da270a3d75f9e8a613d6d88b3b2967fc8e1fb68 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 12 Aug 2020 16:27:09 -0700 Subject: [PATCH] feat: add 'cascadeTerminateToConfigurations' for better self-host teardown See https://github.com/microsoft/vscode/issues/100368 --- OPTIONS.md | 24 ++++++++----- src/build/generate-contributions.ts | 9 +++++ src/build/strings.ts | 3 ++ src/configuration.ts | 7 ++++ src/extension.ts | 2 ++ src/ui/cascadeTerminateTracker.ts | 36 +++++++++++++++++++ src/ui/customBreakpointsUI.ts | 12 ++++--- src/ui/debugSessionTracker.ts | 52 +++++++++++++++++++++++++--- src/ui/prettyPrint.ts | 4 +-- src/ui/profiling/uiProfileManager.ts | 2 +- src/ui/revealPage.ts | 2 +- src/ui/ui-ioc.ts | 2 ++ 12 files changed, 134 insertions(+), 21 deletions(-) create mode 100644 src/ui/cascadeTerminateTracker.ts diff --git a/OPTIONS.md b/OPTIONS.md index ec7a681ce..7decc7a20 100644 --- a/OPTIONS.md +++ b/OPTIONS.md @@ -5,7 +5,8 @@

address

TCP/IP address of process to be debugged. Default is 'localhost'.

Default value:
"localhost"

attachExistingChildren

Whether to attempt to attach to already-spawned child processes.

Default value:
true

autoAttachChildProcesses

Attach debugger to new child processes automatically.

-
Default value:
true

continueOnAttach

If true, we'll automatically resume programs launched and waiting on --inspect-brk

+
Default value:
true

cascadeTerminateToConfigurations

A list of debug sessions which, when this debug session is terminated, will also be stopped.

+
Default value:
[]

continueOnAttach

If true, we'll automatically resume programs launched and waiting on --inspect-brk

Default value:
false

cwd

Absolute path to the working directory of the program being debugged.

Default value:
"${workspaceFolder}"

enableContentValidation

Toggles whether we verify the contents of files on disk match the ones loaded in the runtime. This is useful in a variety of scenarios and required in some, but can cause issues if you have server-side transformation of scripts, for example.

Default value:
true

env

Environment variables passed to the program. The value null removes the variable from the environment.

@@ -46,7 +47,8 @@

args

Command line arguments passed to the program.

Default value:
[]

attachSimplePort

If set, attaches to the process via the given port. This is generally no longer necessary for Node.js programs and loses the ability to debug child processes, but can be useful in more esoteric scenarios such as with Deno and Docker launches. If set to 0, a random port will be chosen and --inspect-brk added to the launch arguments automatically.

Default value:
null

autoAttachChildProcesses

Attach debugger to new child processes automatically.

-
Default value:
true

console

Where to launch the debug target.

+
Default value:
true

cascadeTerminateToConfigurations

A list of debug sessions which, when this debug session is terminated, will also be stopped.

+
Default value:
[]

console

Where to launch the debug target.

Default value:
"internalConsole"

cwd

Absolute path to the working directory of the program being debugged.

Default value:
"${workspaceFolder}"

enableContentValidation

Toggles whether we verify the contents of files on disk match the ones loaded in the runtime. This is useful in a variety of scenarios and required in some, but can cause issues if you have server-side transformation of scripts, for example.

Default value:
true

env

Environment variables passed to the program. The value null removes the variable from the environment.

@@ -89,7 +91,8 @@ ### node-terminal: launch

autoAttachChildProcesses

Attach debugger to new child processes automatically.

-
Default value:
true

command

Command to run in the launched terminal. If not provided, the terminal will open without launching a program.

+
Default value:
true

cascadeTerminateToConfigurations

A list of debug sessions which, when this debug session is terminated, will also be stopped.

+
Default value:
[]

command

Command to run in the launched terminal. If not provided, the terminal will open without launching a program.

Default value:
undefined

cwd

Absolute path to the working directory of the program being debugged.

Default value:
"${workspaceFolder}"

enableContentValidation

Toggles whether we verify the contents of files on disk match the ones loaded in the runtime. This is useful in a variety of scenarios and required in some, but can cause issues if you have server-side transformation of scripts, for example.

Default value:
true

env

Environment variables passed to the program. The value null removes the variable from the environment.

@@ -130,7 +133,8 @@
Default value:
[
   "--extensionDevelopmentPath=${workspaceFolder}"
 ]

autoAttachChildProcesses

Attach debugger to new child processes automatically.

-
Default value:
false

cwd

Absolute path to the working directory of the program being debugged.

+
Default value:
false

cascadeTerminateToConfigurations

A list of debug sessions which, when this debug session is terminated, will also be stopped.

+
Default value:
[]

cwd

Absolute path to the working directory of the program being debugged.

Default value:
"${workspaceFolder}"

debugWebviews

Configures whether we should try to attach to webviews in the launched VS Code instance. Note: at the moment this requires the setting "webview.experimental.useExternalEndpoint": true to work properly, and will only work in desktop VS Code.

Default value:
false

debugWebWorkerHost

Configures whether we should try to attach to the web worker extension host.

Default value:
false

enableContentValidation

Toggles whether we verify the contents of files on disk match the ones loaded in the runtime. This is useful in a variety of scenarios and required in some, but can cause issues if you have server-side transformation of scripts, for example.

@@ -168,7 +172,8 @@ ### pwa-chrome: launch

browserLaunchLocation

Forces the browser to be launched in one location. In a remote workspace (through ssh or WSL, for example) this can be used to open the browser on the remote machine rather than locally.

-
Default value:
"workspace"

cleanUp

What clean-up to do after the debugging session finishes. Close only the tab being debug, vs. close the whole browser.

+
Default value:
"workspace"

cascadeTerminateToConfigurations

A list of debug sessions which, when this debug session is terminated, will also be stopped.

+
Default value:
[]

cleanUp

What clean-up to do after the debugging session finishes. Close only the tab being debug, vs. close the whole browser.

Default value:
"wholeBrowser"

cwd

Optional working directory for the runtime executable.

Default value:
null

disableNetworkCache

Controls whether to skip the network cache for each request

Default value:
true

enableContentValidation

Toggles whether we verify the contents of files on disk match the ones loaded in the runtime. This is useful in a variety of scenarios and required in some, but can cause issues if you have server-side transformation of scripts, for example.

@@ -214,7 +219,8 @@

address

IP address or hostname the debugged browser is listening on.

Default value:
"localhost"

browserAttachLocation

Forces the browser to attach in one location. In a remote workspace (through ssh or WSL, for example) this can be used to attach to a browser on the remote machine rather than locally.

-
Default value:
"workspace"

disableNetworkCache

Controls whether to skip the network cache for each request

+
Default value:
"workspace"

cascadeTerminateToConfigurations

A list of debug sessions which, when this debug session is terminated, will also be stopped.

+
Default value:
[]

disableNetworkCache

Controls whether to skip the network cache for each request

Default value:
true

enableContentValidation

Toggles whether we verify the contents of files on disk match the ones loaded in the runtime. This is useful in a variety of scenarios and required in some, but can cause issues if you have server-side transformation of scripts, for example.

Default value:
true

inspectUri

Format to use to rewrite the inspectUri: It's a template string that interpolates keys in {curlyBraces}. Available keys are:
- url.* is the parsed address of the running application. For instance, {url.port}, {url.hostname}
- port is the debug port that Chrome is listening on.
- browserInspectUri is the inspector URI on the launched browser
- wsProtocol is the hinted websocket protocol. This is set to wss if the original URL is https, or ws otherwise.

Default value:
undefined

outFiles

If source maps are enabled, these glob patterns specify the generated JavaScript files. If a pattern starts with ! the files are excluded. If not specified, the generated code is expected in the same directory as its source.

@@ -253,7 +259,8 @@

address

When debugging webviews, the IP address or hostname the webview is listening on. Will be automatically discovered if not set.

Default value:
"localhost"

browserLaunchLocation

Forces the browser to be launched in one location. In a remote workspace (through ssh or WSL, for example) this can be used to open the browser on the remote machine rather than locally.

-
Default value:
"workspace"

cleanUp

What clean-up to do after the debugging session finishes. Close only the tab being debug, vs. close the whole browser.

+
Default value:
"workspace"

cascadeTerminateToConfigurations

A list of debug sessions which, when this debug session is terminated, will also be stopped.

+
Default value:
[]

cleanUp

What clean-up to do after the debugging session finishes. Close only the tab being debug, vs. close the whole browser.

Default value:
"wholeBrowser"

cwd

Optional working directory for the runtime executable.

Default value:
null

disableNetworkCache

Controls whether to skip the network cache for each request

Default value:
true

enableContentValidation

Toggles whether we verify the contents of files on disk match the ones loaded in the runtime. This is useful in a variety of scenarios and required in some, but can cause issues if you have server-side transformation of scripts, for example.

@@ -300,7 +307,8 @@

address

IP address or hostname the debugged browser is listening on.

Default value:
"localhost"

browserAttachLocation

Forces the browser to attach in one location. In a remote workspace (through ssh or WSL, for example) this can be used to attach to a browser on the remote machine rather than locally.

-
Default value:
"workspace"

disableNetworkCache

Controls whether to skip the network cache for each request

+
Default value:
"workspace"

cascadeTerminateToConfigurations

A list of debug sessions which, when this debug session is terminated, will also be stopped.

+
Default value:
[]

disableNetworkCache

Controls whether to skip the network cache for each request

Default value:
true

enableContentValidation

Toggles whether we verify the contents of files on disk match the ones loaded in the runtime. This is useful in a variety of scenarios and required in some, but can cause issues if you have server-side transformation of scripts, for example.

Default value:
true

inspectUri

Format to use to rewrite the inspectUri: It's a template string that interpolates keys in {curlyBraces}. Available keys are:
- url.* is the parsed address of the running application. For instance, {url.port}, {url.hostname}
- port is the debug port that Chrome is listening on.
- browserInspectUri is the inspector URI on the launched browser
- wsProtocol is the hinted websocket protocol. This is set to wss if the original URL is https, or ws otherwise.

Default value:
undefined

outFiles

If source maps are enabled, these glob patterns specify the generated JavaScript files. If a pattern starts with ! the files are excluded. If not specified, the generated code is expected in the same directory as its source.

diff --git a/src/build/generate-contributions.ts b/src/build/generate-contributions.ts index 2bf263a20..c6ccaa0b3 100644 --- a/src/build/generate-contributions.ts +++ b/src/build/generate-contributions.ts @@ -259,6 +259,15 @@ const baseConfigurationAttributes: ConfigurationAttributes = type: 'boolean', description: refString('enableContentValidation.description'), }, + cascadeTerminateToConfigurations: { + type: 'array', + items: { + type: 'string', + uniqueItems: true, + }, + default: [], + description: refString('base.cascadeTerminateToConfigurations.label'), + }, }; /** diff --git a/src/build/strings.ts b/src/build/strings.ts index 61668c299..ffd49a0bd 100644 --- a/src/build/strings.ts +++ b/src/build/strings.ts @@ -21,6 +21,9 @@ const strings = { 'trace.stdio.description': 'Whether to return trace data from the launched application or browser.', + 'base.cascadeTerminateToConfigurations.label': + 'A list of debug sessions which, when this debug session is terminated, will also be stopped.', + 'extensionHost.label': 'VS Code Extension Development (preview)', 'extensionHost.launch.config.name': 'Launch Extension', 'extensionHost.launch.env.description': 'Environment variables passed to the extension host.', diff --git a/src/configuration.ts b/src/configuration.ts index 2ca1903d6..49a74e3f6 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -165,6 +165,12 @@ export interface IBaseConfiguration extends IMandatedConfiguration { */ enableContentValidation: boolean; + /** + * A list of debug sessions which, when this debug session is terminated, + * will also be stopped. + */ + cascadeTerminateToConfigurations: string[]; + /** * The value of the ${workspaceFolder} variable */ @@ -752,6 +758,7 @@ export const baseDefaults: IBaseConfiguration = { outFiles: ['${workspaceFolder}/**/*.js', '!**/node_modules/**'], sourceMapPathOverrides: defaultSourceMapPathOverrides('${workspaceFolder}'), enableContentValidation: true, + cascadeTerminateToConfigurations: [], // Should always be determined upstream; __workspaceFolder: '', __autoExpandGetters: false, diff --git a/src/extension.ts b/src/extension.ts index 8814fb6df..e9a53012c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -9,6 +9,7 @@ import { extensionId } from './configuration'; import { createGlobalContainer } from './ioc'; import { DelegateLauncherFactory } from './targets/delegate/delegateLauncherFactory'; import { registerAutoAttach } from './ui/autoAttach'; +import { CascadeTerminationTracker } from './ui/cascadeTerminateTracker'; import { registerCompanionBrowserLaunch } from './ui/companionBrowserLaunch'; import { IDebugConfigurationProvider, IDebugConfigurationResolver } from './ui/configuration'; import { registerCustomBreakpointsUI } from './ui/customBreakpointsUI'; @@ -99,6 +100,7 @@ export function activate(context: vscode.ExtensionContext) { registerAutoAttach(context, services.get(DelegateLauncherFactory)); registerRevealPage(context, debugSessionTracker); services.get(DebugLinkUi).register(context); + services.get(CascadeTerminationTracker).register(context); } export function deactivate() { diff --git a/src/ui/cascadeTerminateTracker.ts b/src/ui/cascadeTerminateTracker.ts new file mode 100644 index 000000000..70b97f44b --- /dev/null +++ b/src/ui/cascadeTerminateTracker.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import { inject, injectable } from 'inversify'; +import * as vscode from 'vscode'; +import { DebugSessionTracker } from './debugSessionTracker'; + +/** + * Watches for sessions to be terminated. When they are, it runs cascading + * termination if configured. + */ +@injectable() +export class CascadeTerminationTracker { + constructor(@inject(DebugSessionTracker) private readonly tracker: DebugSessionTracker) {} + + /** + * Registers the tracker for the extension. + */ + public register(context: vscode.ExtensionContext) { + context.subscriptions.push( + this.tracker.onSessionEnded(session => { + const targets: string[] = session.configuration.cascadeTerminateToConfigurations; + if (!targets || !(targets instanceof Array)) { + return; // may be a nested session + } + + for (const configName of targets) { + for (const session of this.tracker.getByName(configName)) { + vscode.debug.stopDebugging(session); + } + } + }), + ); + } +} diff --git a/src/ui/customBreakpointsUI.ts b/src/ui/customBreakpointsUI.ts index 2b0938ee9..2ac0f3b07 100644 --- a/src/ui/customBreakpointsUI.ts +++ b/src/ui/customBreakpointsUI.ts @@ -4,13 +4,13 @@ import * as vscode from 'vscode'; import { - ICustomBreakpoint, CustomBreakpointId, customBreakpoints, + ICustomBreakpoint, } from '../adapter/customBreakpoints'; +import { Commands, Contributions } from '../common/contributionUtils'; import { EventEmitter } from '../common/events'; import { DebugSessionTracker } from './debugSessionTracker'; -import { Contributions, Commands } from '../common/contributionUtils'; class Breakpoint { id: CustomBreakpointId; @@ -45,6 +45,10 @@ class BreakpointsDataProvider implements vscode.TreeDataProvider { this._debugSessionTracker = debugSessionTracker; debugSessionTracker.onSessionAdded(session => { + if (!DebugSessionTracker.isConcreteSession(session)) { + return; + } + session.customRequest('enableCustomBreakpoints', { ids: this.breakpoints.filter(b => b.enabled).map(b => b.id), }); @@ -67,7 +71,7 @@ class BreakpointsDataProvider implements vscode.TreeDataProvider { addBreakpoints(breakpoints: Breakpoint[]) { for (const breakpoint of breakpoints) breakpoint.enabled = true; const ids = breakpoints.map(b => b.id); - for (const session of this._debugSessionTracker.sessions.values()) + for (const session of this._debugSessionTracker.getConcreteSessions()) session.customRequest('enableCustomBreakpoints', { ids }); this._onDidChangeTreeData.fire(undefined); } @@ -77,7 +81,7 @@ class BreakpointsDataProvider implements vscode.TreeDataProvider { for (const breakpoint of this.breakpoints) { if (ids.has(breakpoint.id)) breakpoint.enabled = false; } - for (const session of this._debugSessionTracker.sessions.values()) + for (const session of this._debugSessionTracker.getConcreteSessions()) session.customRequest('disableCustomBreakpoints', { ids: breakpointIds }); this._onDidChangeTreeData.fire(undefined); } diff --git a/src/ui/debugSessionTracker.ts b/src/ui/debugSessionTracker.ts index e1350904c..22e465e97 100644 --- a/src/ui/debugSessionTracker.ts +++ b/src/ui/debugSessionTracker.ts @@ -2,26 +2,64 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ +import { injectable } from 'inversify'; import * as vscode from 'vscode'; -import Dap from '../dap/api'; import { isDebugType } from '../common/contributionUtils'; -import { injectable } from 'inversify'; +import Dap from '../dap/api'; /** * Keeps a list of known js-debug sessions. */ @injectable() export class DebugSessionTracker implements vscode.Disposable { + /** + * Returns whether the session is a concrete debug + * session -- that is, not a logical session wrapper. + */ + public static isConcreteSession(session: vscode.DebugSession) { + return !!session.configuration.__pendingTargetId; + } + private _onSessionAddedEmitter = new vscode.EventEmitter(); + private _onSessionEndedEmitter = new vscode.EventEmitter(); private _disposables: vscode.Disposable[] = []; + private readonly sessions = new Map(); - public sessions = new Map(); + /** + * Fires when any new js-debug session comes in. + */ public onSessionAdded = this._onSessionAddedEmitter.event; + /** + * Fires when any js-debug session ends. + */ + public onSessionEnded = this._onSessionEndedEmitter.event; + + /** + * Returns the session with the given ID. + */ + public getById(id: string) { + return this.sessions.get(id); + } + + /** + * Returns a list of sessions with the given debug session name. + */ + public getByName(name: string) { + return [...this.sessions.values()].filter(s => s.name === name); + } + + /** + * Gets physical debug sessions -- that is, avoids the logical session wrapper. + */ + public getConcreteSessions() { + return [...this.sessions.values()].filter(DebugSessionTracker.isConcreteSession); + } + public attach() { vscode.debug.onDidStartDebugSession( session => { - if (session.configuration.__pendingTargetId) { + if (isDebugType(session.type)) { this.sessions.set(session.id, session); this._onSessionAddedEmitter.fire(session); } @@ -32,12 +70,16 @@ export class DebugSessionTracker implements vscode.Disposable { vscode.debug.onDidTerminateDebugSession( session => { - this.sessions.delete(session.id); + if (isDebugType(session.type)) { + this.sessions.delete(session.id); + this._onSessionEndedEmitter.fire(session); + } }, undefined, this._disposables, ); + // todo: move this into its own class vscode.debug.onDidReceiveDebugSessionCustomEvent( event => { if (!isDebugType(event.session.type)) { diff --git a/src/ui/prettyPrint.ts b/src/ui/prettyPrint.ts index 49466d43c..511190be1 100644 --- a/src/ui/prettyPrint.ts +++ b/src/ui/prettyPrint.ts @@ -71,7 +71,7 @@ export class PrettyPrintTrackerFactory implements vscode.DebugAdapterTrackerFact } const { sessionId, source } = sourceForUri(editor.document.uri); - const session = sessionId && this.tracker.sessions.get(sessionId); + const session = sessionId && this.tracker.getById(sessionId); // For ephemeral files, they're attached to a single session, so go ahead // and send it to the owning session. For files on disk, send it to all @@ -79,7 +79,7 @@ export class PrettyPrintTrackerFactory implements vscode.DebugAdapterTrackerFact if (session) { sendPrintCommand(session, source, editor.selection.start); } else { - for (const session of this.tracker.sessions.values()) { + for (const session of this.tracker.getConcreteSessions()) { sendPrintCommand(session, source, editor.selection.start); } } diff --git a/src/ui/profiling/uiProfileManager.ts b/src/ui/profiling/uiProfileManager.ts index 30e616ff9..2e7157d5f 100644 --- a/src/ui/profiling/uiProfileManager.ts +++ b/src/ui/profiling/uiProfileManager.ts @@ -113,7 +113,7 @@ export class UiProfileManager implements IDisposable { */ public async start(args: IStartProfileArguments) { let maybeSession: vscode.DebugSession | undefined; - const candidates = [...this.tracker.sessions.values()].filter(isProfileCandidate); + const candidates = [...this.tracker.getConcreteSessions()].filter(isProfileCandidate); if (args.sessionId) { maybeSession = candidates.find(s => s.id === args.sessionId); } else { diff --git a/src/ui/revealPage.ts b/src/ui/revealPage.ts index d2a359ba8..5142a01d8 100644 --- a/src/ui/revealPage.ts +++ b/src/ui/revealPage.ts @@ -12,7 +12,7 @@ export const registerRevealPage = ( ) => { context.subscriptions.push( registerCommand(vscode.commands, Commands.RevealPage, async sessionId => { - const session = tracker.sessions.get(sessionId); + const session = tracker.getById(sessionId); await session?.customRequest('revealPage'); }), ); diff --git a/src/ui/ui-ioc.ts b/src/ui/ui-ioc.ts index 0249385d9..7a16146f4 100644 --- a/src/ui/ui-ioc.ts +++ b/src/ui/ui-ioc.ts @@ -18,6 +18,7 @@ import { ManualTerminationConditionFactory } from './profiling/manualTermination import { ITerminationConditionFactory } from './profiling/terminationCondition'; import { UiProfileManager } from './profiling/uiProfileManager'; import { TerminalLinkHandler } from './terminalLinkHandler'; +import { CascadeTerminationTracker } from './cascadeTerminateTracker'; export const registerUiComponents = (container: Container) => { allConfigurationResolvers.forEach(cls => { @@ -36,6 +37,7 @@ export const registerUiComponents = (container: Container) => { container.bind(UiProfileManager).toSelf().inSingletonScope().onActivation(trackDispose); container.bind(TerminalLinkHandler).toSelf().inSingletonScope(); container.bind(DebugLinkUi).toSelf().inSingletonScope(); + container.bind(CascadeTerminationTracker).toSelf().inSingletonScope(); container .bind(ITerminationConditionFactory)