Skip to content

Commit 1755226

Browse files
authored
Add support for debugging on a remote jupyter server (microsoft#6549)
* First steps in remote support for debugging * Add comment to help mitigate mismatches * Change to local host for remote enable attach * Handle path mapping * Fix live share to attach * Fix guest debugging to not actually start the debugger * Add news entry * Fix localize test * Add remote test for debugging * Review feedback
1 parent f96bf83 commit 1755226

25 files changed

+425
-157
lines changed

news/1 Enhancements/6379.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for remote debugging of jupyter cells.

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1400,6 +1400,12 @@
14001400
"description": "When debugging a cell, stop on the first line if no other breakpoints in the current cell.",
14011401
"scope": "resource"
14021402
},
1403+
"python.dataScience.remoteDebuggerPort": {
1404+
"type": "number",
1405+
"default": -1,
1406+
"description": "When debugging a cell, open this port on the remote box. If -1 is specified, a random port between 8889 and 9000 will be attempted.",
1407+
"scope": "resource"
1408+
},
14031409
"python.dataScience.textOutputLineLimit": {
14041410
"type": "number",
14051411
"default": 10000,

package.nls.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,5 +302,10 @@
302302
"DataScience.jupyterDataRateExceeded": "Cannot view variable because data rate exceeded. Please restart your server with a higher data rate limit. For example, --NotebookApp.iopub_data_rate_limit=10000000000.0",
303303
"DataScience.addCellBelowCommandTitle": "Add cell",
304304
"DataScience.debugCellCommandTitle": "Debug cell",
305-
"DataScience.variableExplorerDisabledDuringDebugging": "Variables are not available while debugging."
305+
"DataScience.variableExplorerDisabledDuringDebugging": "Variables are not available while debugging.",
306+
"DataScience.jupyterDebuggerNotInstalledError" : "Pip module ptvsd is required for debugging cells. You will need to install it to debug cells.",
307+
"DataScience.jupyterDebuggerPortNotAvailableError" : "Port {0} cannot be opened for debugging. Please specify a different port in the remoteDebuggerPort setting.",
308+
"DataScience.jupyterDebuggerPortBlockedError" : "Port {0} cannot be connected to for debugging. Please let port {0} through your firewall.",
309+
"DataScience.jupyterDebuggerPortNotAvailableSearchError" : "Ports in the range {0}-{1} cannot be found for debugging. Please specify a port in the remoteDebuggerPort setting.",
310+
"DataScience.jupyterDebuggerPortBlockedSearchError" : "A port cannot be connected to for debugging. Please let ports {0}-{1} through your firewall."
306311
}

src/client/common/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ export interface IDataScienceSettings {
328328
stopOnFirstLineWhileDebugging?: boolean;
329329
textOutputLineLimit?: number;
330330
magicCommandsAsComments?: boolean;
331+
remoteDebuggerPort?: number;
331332
}
332333

333334
export const IConfigurationService = Symbol('IConfigurationService');

src/client/common/utils/localize.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,11 @@ export namespace DataScience {
229229
export const addCellBelowCommandTitle = localize('DataScience.addCellBelowCommandTitle', 'Add cell');
230230
export const debugCellCommandTitle = localize('DataScience.debugCellCommandTitle', 'Debug cell');
231231
export const variableExplorerDisabledDuringDebugging = localize('DataScience.variableExplorerDisabledDuringDebugging', 'Variables are not available while debugging.');
232+
export const jupyterDebuggerNotInstalledError = localize('DataScience.jupyterDebuggerNotInstalledError', 'Pip module ptvsd is required for debugging cells. You will need to install it to debug cells.');
233+
export const jupyterDebuggerPortNotAvailableError = localize('DataScience.jupyterDebuggerPortNotAvailableError', 'Port {0} cannot be opened for debugging. Please specify a different port in the remoteDebuggerPort setting.');
234+
export const jupyterDebuggerPortBlockedError = localize('DataScience.jupyterDebuggerPortBlockedError', 'Port {0} cannot be connected to for debugging. Please let port {0} through your firewall.');
235+
export const jupyterDebuggerPortNotAvailableSearchError = localize('DataScience.jupyterDebuggerPortNotAvailableSearchError', 'Ports in the range {0}-{1} cannot be found for debugging. Please specify a port in the remoteDebuggerPort setting.');
236+
export const jupyterDebuggerPortBlockedSearchError = localize('DataScience.jupyterDebuggerPortBlockedSearchError', 'A port cannot be connected to for debugging. Please let ports {0}-{1} through your firewall.');
232237
}
233238

234239
export namespace DebugConfigStrings {

src/client/datascience/constants.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export namespace RegExpValues {
5454
export const KernelSpecOutputRegEx = /^\s*(\S+)\s+(\S+)$/;
5555
// This next one has to be a string because uglifyJS isn't handling the groups. We use named-js-regexp to parse it
5656
// instead.
57-
export const UrlPatternRegEx = '(?<PREFIX>https?:\\/\\/)((\\(.+\\s+or\\s+(?<IP>.+)\\))|(?<LOCAL>[^\\s]+))(?<REST>:.+)' ;
57+
export const UrlPatternRegEx = '(?<PREFIX>https?:\\/\\/)((\\(.+\\s+or\\s+(?<IP>.+)\\))|(?<LOCAL>[^\\s]+))(?<REST>:.+)';
5858
export interface IUrlPatternGroupType {
5959
LOCAL: string | undefined;
6060
PREFIX: string | undefined;
@@ -145,7 +145,7 @@ export enum Telemetry {
145145
WebviewMonacoStyleUpdate = 'DATASCIENCE.WEBVIEW_MONACO_STYLE_UPDATE',
146146
DataViewerFetchTime = 'DATASCIENCE.DATAVIEWER_FETCH_TIME',
147147
FindJupyterKernelSpec = 'DATASCIENCE.FIND_JUPYTER_KERNEL_SPEC'
148-
}
148+
}
149149

150150
export namespace HelpLinks {
151151
export const PythonInteractiveHelpLink = 'https://aka.ms/pyaiinstall';
@@ -155,6 +155,8 @@ export namespace HelpLinks {
155155
export namespace Settings {
156156
export const JupyterServerLocalLaunch = 'local';
157157
export const IntellisenseTimeout = 300;
158+
export const RemoteDebuggerPortBegin = 8889;
159+
export const RemoteDebuggerPortEnd = 9000;
158160
}
159161

160162
export namespace Identifiers {

src/client/datascience/interactive-window/interactiveWindow.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,7 @@ export class InteractiveWindow extends WebViewHost<IInteractiveWindowMapping> im
615615
sendTelemetryEvent(Telemetry.RemoteAddCode);
616616

617617
// Submit this item as new code.
618-
this.submitCode(args.code, args.file, args.line, args.id).ignoreErrors();
618+
this.submitCode(args.code, args.file, args.line, args.id, undefined, undefined, args.debug).ignoreErrors();
619619
}
620620
}
621621

@@ -642,7 +642,6 @@ export class InteractiveWindow extends WebViewHost<IInteractiveWindowMapping> im
642642
if (this.jupyterServer) {
643643
await this.jupyterServer.restartKernel(this.generateDataScienceExtraSettings().jupyterInterruptTimeout);
644644
await this.addSysInfo(SysInfoReason.Restart);
645-
await this.jupyterDebugger.enableAttach(this.jupyterServer);
646645

647646
// Compute if dark or not.
648647
const knownDark = await this.isDark();
@@ -702,7 +701,7 @@ export class InteractiveWindow extends WebViewHost<IInteractiveWindowMapping> im
702701

703702
// Activate the other side, and send as if came from a file
704703
this.interactiveWindowProvider.getOrCreateActive().then(_v => {
705-
this.shareMessage(InteractiveWindowMessages.RemoteAddCode, { code: info.code, file: Identifiers.EmptyFileName, line: 0, id: info.id, originator: this.id });
704+
this.shareMessage(InteractiveWindowMessages.RemoteAddCode, { code: info.code, file: Identifiers.EmptyFileName, line: 0, id: info.id, originator: this.id, debug: false });
706705
}).ignoreErrors();
707706
}
708707
}
@@ -716,7 +715,7 @@ export class InteractiveWindow extends WebViewHost<IInteractiveWindowMapping> im
716715
// Transmit this submission to all other listeners (in a live share session)
717716
if (!id) {
718717
id = uuid();
719-
this.shareMessage(InteractiveWindowMessages.RemoteAddCode, { code, file, line, id, originator: this.id });
718+
this.shareMessage(InteractiveWindowMessages.RemoteAddCode, { code, file, line, id, originator: this.id, debug: debug !== undefined ? debug : false });
720719
}
721720

722721
// Create a deferred object that will wait until the status is disposed
@@ -1054,11 +1053,6 @@ export class InteractiveWindow extends WebViewHost<IInteractiveWindowMapping> im
10541053
// Now try to create a notebook server
10551054
this.jupyterServer = await this.jupyterExecution.connectToNotebookServer(options);
10561055

1057-
// Enable debugging support if set
1058-
if (this.jupyterServer) {
1059-
await this.jupyterDebugger.enableAttach(this.jupyterServer);
1060-
}
1061-
10621056
// Before we run any cells, update the dark setting
10631057
if (this.jupyterServer) {
10641058
await this.jupyterServer.setMatplotLibStyle(knownDark);
@@ -1152,6 +1146,9 @@ export class InteractiveWindow extends WebViewHost<IInteractiveWindowMapping> im
11521146
// For a restart, tell our window to reset
11531147
if (reason === SysInfoReason.Restart || reason === SysInfoReason.New) {
11541148
this.postMessage(InteractiveWindowMessages.RestartKernel).ignoreErrors();
1149+
if (this.jupyterServer) {
1150+
this.jupyterDebugger.onRestart(this.jupyterServer);
1151+
}
11551152
}
11561153

11571154
this.logger.logInformation(`Sys info for ${this.id} ${reason} complete`);

src/client/datascience/interactive-window/interactiveWindowTypes.ts

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export namespace InteractiveWindowMessages {
6262

6363
// These are the messages that will mirror'd to guest/hosts in
6464
// a live share session
65-
export const InteractiveWindowRemoteMessages : string[] = [
65+
export const InteractiveWindowRemoteMessages: string[] = [
6666
InteractiveWindowMessages.AddedSysInfo,
6767
InteractiveWindowMessages.RemoteAddCode
6868
];
@@ -94,6 +94,7 @@ export interface IExecuteInfo {
9494
id: string;
9595
file: string;
9696
line: number;
97+
debug: boolean;
9798
}
9899

99100
export interface IRemoteAddCode extends IExecuteInfo {
@@ -196,27 +197,27 @@ export class IInteractiveWindowMapping {
196197
public [InteractiveWindowMessages.Started]: never | undefined;
197198
public [InteractiveWindowMessages.AddedSysInfo]: IAddedSysInfo;
198199
public [InteractiveWindowMessages.RemoteAddCode]: IRemoteAddCode;
199-
public [InteractiveWindowMessages.Activate] : never | undefined;
200+
public [InteractiveWindowMessages.Activate]: never | undefined;
200201
public [InteractiveWindowMessages.ShowDataViewer]: IShowDataViewer;
201202
public [InteractiveWindowMessages.GetVariablesRequest]: number;
202203
public [InteractiveWindowMessages.GetVariablesResponse]: IJupyterVariablesResponse;
203204
public [InteractiveWindowMessages.GetVariableValueRequest]: IJupyterVariable;
204205
public [InteractiveWindowMessages.GetVariableValueResponse]: IJupyterVariable;
205206
public [InteractiveWindowMessages.VariableExplorerToggle]: boolean;
206-
public [CssMessages.GetCssRequest] : IGetCssRequest;
207-
public [CssMessages.GetCssResponse] : IGetCssResponse;
208-
public [InteractiveWindowMessages.ProvideCompletionItemsRequest] : IProvideCompletionItemsRequest;
209-
public [InteractiveWindowMessages.CancelCompletionItemsRequest] : ICancelIntellisenseRequest;
210-
public [InteractiveWindowMessages.ProvideCompletionItemsResponse] : IProvideCompletionItemsResponse;
211-
public [InteractiveWindowMessages.ProvideHoverRequest] : IProvideHoverRequest;
212-
public [InteractiveWindowMessages.CancelHoverRequest] : ICancelIntellisenseRequest;
213-
public [InteractiveWindowMessages.ProvideHoverResponse] : IProvideHoverResponse;
214-
public [InteractiveWindowMessages.ProvideSignatureHelpRequest] : IProvideSignatureHelpRequest;
215-
public [InteractiveWindowMessages.CancelSignatureHelpRequest] : ICancelIntellisenseRequest;
216-
public [InteractiveWindowMessages.ProvideSignatureHelpResponse] : IProvideSignatureHelpResponse;
217-
public [InteractiveWindowMessages.AddCell] : IAddCell;
218-
public [InteractiveWindowMessages.EditCell] : IEditCell;
219-
public [InteractiveWindowMessages.RemoveCell] : IRemoveCell;
207+
public [CssMessages.GetCssRequest]: IGetCssRequest;
208+
public [CssMessages.GetCssResponse]: IGetCssResponse;
209+
public [InteractiveWindowMessages.ProvideCompletionItemsRequest]: IProvideCompletionItemsRequest;
210+
public [InteractiveWindowMessages.CancelCompletionItemsRequest]: ICancelIntellisenseRequest;
211+
public [InteractiveWindowMessages.ProvideCompletionItemsResponse]: IProvideCompletionItemsResponse;
212+
public [InteractiveWindowMessages.ProvideHoverRequest]: IProvideHoverRequest;
213+
public [InteractiveWindowMessages.CancelHoverRequest]: ICancelIntellisenseRequest;
214+
public [InteractiveWindowMessages.ProvideHoverResponse]: IProvideHoverResponse;
215+
public [InteractiveWindowMessages.ProvideSignatureHelpRequest]: IProvideSignatureHelpRequest;
216+
public [InteractiveWindowMessages.CancelSignatureHelpRequest]: ICancelIntellisenseRequest;
217+
public [InteractiveWindowMessages.ProvideSignatureHelpResponse]: IProvideSignatureHelpResponse;
218+
public [InteractiveWindowMessages.AddCell]: IAddCell;
219+
public [InteractiveWindowMessages.EditCell]: IEditCell;
220+
public [InteractiveWindowMessages.RemoveCell]: IRemoveCell;
220221
public [InteractiveWindowMessages.LoadOnigasmAssemblyRequest]: never | undefined;
221222
public [InteractiveWindowMessages.LoadOnigasmAssemblyResponse]: Buffer;
222223
public [InteractiveWindowMessages.LoadTmLanguageRequest]: never | undefined;

src/client/datascience/jupyter/jupyterConnection.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class JupyterConnectionWaiter {
4242
private fileSystem: IFileSystem;
4343
private notebook_dir: string;
4444
private getServerInfo: (cancelToken?: CancellationToken) => Promise<JupyterServerInfo[] | undefined>;
45-
private createConnection: (b: string, t: string, p: Disposable) => IConnection;
45+
private createConnection: (b: string, t: string, h: string, p: Disposable) => IConnection;
4646
private launchResult: ObservableExecutionResult<string>;
4747
private cancelToken: CancellationToken | undefined;
4848
private stderr: string[] = [];
@@ -52,7 +52,7 @@ class JupyterConnectionWaiter {
5252
launchResult: ObservableExecutionResult<string>,
5353
notebookFile: string,
5454
getServerInfo: (cancelToken?: CancellationToken) => Promise<JupyterServerInfo[] | undefined>,
55-
createConnection: (b: string, t: string, p: Disposable) => IConnection,
55+
createConnection: (b: string, t: string, h: string, p: Disposable) => IConnection,
5656
serviceContainer: IServiceContainer,
5757
cancelToken?: CancellationToken
5858
) {
@@ -124,7 +124,8 @@ class JupyterConnectionWaiter {
124124
if (matchInfo) {
125125
const url = matchInfo.url;
126126
const token = matchInfo.token;
127-
this.resolveStartPromise(url, token);
127+
const host = matchInfo.hostname;
128+
this.resolveStartPromise(url, token, host);
128129
}
129130
}
130131

@@ -158,7 +159,7 @@ class JupyterConnectionWaiter {
158159
}
159160

160161
// Here we parsed the URL correctly
161-
this.resolveStartPromise(`${url.protocol}//${url.host}${url.pathname}`, `${url.searchParams.get('token')}`);
162+
this.resolveStartPromise(`${url.protocol}//${url.host}${url.pathname}`, `${url.searchParams.get('token')}`, url.hostname);
162163
}
163164
}
164165

@@ -187,11 +188,11 @@ class JupyterConnectionWaiter {
187188
}
188189
}
189190

190-
private resolveStartPromise = (baseUrl: string, token: string) => {
191+
private resolveStartPromise = (baseUrl: string, token: string, hostName: string) => {
191192
// tslint:disable-next-line: no-any
192193
clearTimeout(this.launchTimeout as any);
193194
if (!this.startPromise.rejected) {
194-
const connection = this.createConnection(baseUrl, token, this.launchResult);
195+
const connection = this.createConnection(baseUrl, token, hostName, this.launchResult);
195196
const origDispose = connection.dispose.bind(connection);
196197
connection.dispose = () => {
197198
// Stop listening when we disconnect
@@ -216,12 +217,14 @@ class JupyterConnectionWaiter {
216217
export class JupyterConnection implements IConnection {
217218
public baseUrl: string;
218219
public token: string;
220+
public hostName: string;
219221
public localLaunch: boolean;
220222
public localProcExitCode: number | undefined;
221223
private disposable: Disposable | undefined;
222224
private eventEmitter: EventEmitter<number> = new EventEmitter<number>();
223-
constructor(baseUrl: string, token: string, disposable: Disposable, childProc: ChildProcess | undefined) {
225+
constructor(baseUrl: string, token: string, hostName: string, disposable: Disposable, childProc: ChildProcess | undefined) {
224226
this.baseUrl = baseUrl;
227+
this.hostName = hostName;
225228
this.token = token;
226229
this.localLaunch = true;
227230
this.disposable = disposable;
@@ -251,7 +254,7 @@ export class JupyterConnection implements IConnection {
251254
notebookExecution,
252255
notebookFile,
253256
getServerInfo,
254-
(baseUrl: string, token: string, processDisposable: Disposable) => new JupyterConnection(baseUrl, token, processDisposable, notebookExecution.proc),
257+
(baseUrl: string, token: string, hostName: string, processDisposable: Disposable) => new JupyterConnection(baseUrl, token, hostName, processDisposable, notebookExecution.proc),
255258
serviceContainer,
256259
cancelToken
257260
);

0 commit comments

Comments
 (0)