Skip to content

Commit b8c2f20

Browse files
Prompt to install ptvsd when needed (microsoft#6562)
1 parent 2454423 commit b8c2f20

File tree

7 files changed

+162
-30
lines changed

7 files changed

+162
-30
lines changed

build/functional-test-requirements.txt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,4 @@ jupyter
44
numpy
55
matplotlib
66
pandas
7-
livelossplot
8-
# This is temporary until these bits are ready
9-
git+https://s3h4llbub6kmnxk53k53ricbg4n7s6tif3ls6qsywersiroezaea@ptvsd.visualstudio.com/ptvsd-pr/_git/ptvsd-pr@ptvsd_jupyter_cells
7+
livelossplot

news/1 Enhancements/6378.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Give the option to install ptvsd if user is missing it and tries to debug

package.nls.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,5 +307,9 @@
307307
"DataScience.jupyterDebuggerPortNotAvailableError" : "Port {0} cannot be opened for debugging. Please specify a different port in the remoteDebuggerPort setting.",
308308
"DataScience.jupyterDebuggerPortBlockedError" : "Port {0} cannot be connected to for debugging. Please let port {0} through your firewall.",
309309
"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."
310+
"DataScience.jupyterDebuggerPortBlockedSearchError" : "A port cannot be connected to for debugging. Please let ports {0}-{1} through your firewall.",
311+
"DataScience.jupyterDebuggerInstallPtvsdNew" : "Pip module ptvsd is required for debugging cells. Install ptvsd and continue to debug cell?",
312+
"DataScience.jupyterDebuggerInstallPtvsdUpdate" : "The version of ptvsd installed does not support debugging cells. Update ptvsd to newest version and continue to debug cell?",
313+
"DataScience.jupyterDebuggerInstallPtvsdYes" : "Yes",
314+
"DataScience.jupyterDebuggerInstallPtvsdNo" : "No"
311315
}

src/client/common/utils/localize.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,10 @@ export namespace DataScience {
234234
export const jupyterDebuggerPortBlockedError = localize('DataScience.jupyterDebuggerPortBlockedError', 'Port {0} cannot be connected to for debugging. Please let port {0} through your firewall.');
235235
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.');
236236
export const jupyterDebuggerPortBlockedSearchError = localize('DataScience.jupyterDebuggerPortBlockedSearchError', 'A port cannot be connected to for debugging. Please let ports {0}-{1} through your firewall.');
237+
export const jupyterDebuggerInstallPtvsdNew = localize('DataScience.jupyterDebuggerInstallPtvsdNew', 'Pip module ptvsd is required for debugging cells. Install ptvsd and continue to debug cell?');
238+
export const jupyterDebuggerInstallPtvsdUpdate = localize('DataScience.jupyterDebuggerInstallPtvsdUpdate', 'The version of ptvsd installed does not support debugging cells. Update ptvsd to newest version and continue to debug cell?');
239+
export const jupyterDebuggerInstallPtvsdYes = localize('DataScience.jupyterDebuggerInstallPtvsdYes', 'Yes');
240+
export const jupyterDebuggerInstallPtvsdNo = localize('DataScience.jupyterDebuggerInstallPtvsdNo', 'No');
237241
}
238242

239243
export namespace DebugConfigStrings {

src/client/datascience/constants.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,10 @@ export enum Telemetry {
144144
WebviewStyleUpdate = 'DATASCIENCE.WEBVIEW_STYLE_UPDATE',
145145
WebviewMonacoStyleUpdate = 'DATASCIENCE.WEBVIEW_MONACO_STYLE_UPDATE',
146146
DataViewerFetchTime = 'DATASCIENCE.DATAVIEWER_FETCH_TIME',
147-
FindJupyterKernelSpec = 'DATASCIENCE.FIND_JUPYTER_KERNEL_SPEC'
147+
FindJupyterKernelSpec = 'DATASCIENCE.FIND_JUPYTER_KERNEL_SPEC',
148+
PtvsdPromptToInstall = 'DATASCIENCE.PTVSD_PROMPT_TO_INSTALL',
149+
PtvsdSuccessfullyInstalled = 'DATASCIENCE.PTVSD_SUCCESSFULLY_INSTALLED',
150+
PtvsdInstallFailed = 'DATASCIENCE.PTVSD_INSTALL_FAILED'
148151
}
149152

150153
export namespace HelpLinks {

src/client/datascience/jupyter/jupyterDebugger.ts

Lines changed: 144 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,21 @@
44
import { nbformat } from '@jupyterlab/coreutils';
55
import { inject, injectable } from 'inversify';
66
import * as net from 'net';
7+
import * as path from 'path';
78
import * as uuid from 'uuid/v4';
89
import { DebugConfiguration } from 'vscode';
910
import * as vsls from 'vsls/vscode';
1011

11-
import { ICommandManager, IDebugService, IWorkspaceService } from '../../common/application/types';
12-
import { traceInfo, traceWarning } from '../../common/logger';
12+
import { IApplicationShell, ICommandManager, IDebugService, IWorkspaceService } from '../../common/application/types';
13+
import { traceError, traceInfo, traceWarning } from '../../common/logger';
1314
import { IPlatformService } from '../../common/platform/types';
1415
import { IConfigurationService } from '../../common/types';
1516
import { createDeferred } from '../../common/utils/async';
17+
import * as localize from '../../common/utils/localize';
18+
import { EXTENSION_ROOT_DIR } from '../../constants';
19+
import { captureTelemetry, sendTelemetryEvent } from '../../telemetry';
1620
import { concatMultilineString } from '../common';
17-
import { Identifiers, Settings } from '../constants';
21+
import { Identifiers, Settings, Telemetry } from '../constants';
1822
import {
1923
CellState,
2024
ICell,
@@ -30,10 +34,18 @@ import { JupyterDebuggerPortBlockedError } from './jupyterDebuggerPortBlockedErr
3034
import { JupyterDebuggerPortNotAvailableError } from './jupyterDebuggerPortNotAvailableError';
3135
import { ILiveShareHasRole } from './liveshare/types';
3236

37+
interface IPtvsdVersion {
38+
major: number;
39+
minor: number;
40+
revision: string;
41+
}
42+
3343
@injectable()
3444
export class JupyterDebugger implements IJupyterDebugger, ICellHashListener {
45+
private requiredPtvsdVersion: IPtvsdVersion = { major: 4, minor: 3, revision: '' };
3546
private configs: Map<string, DebugConfiguration> = new Map<string, DebugConfiguration>();
3647
constructor(
48+
@inject(IApplicationShell) private appShell: IApplicationShell,
3749
@inject(IConfigurationService) private configService: IConfigurationService,
3850
@inject(ICommandManager) private commandManager: ICommandManager,
3951
@inject(IDebugService) private debugService: IDebugService,
@@ -47,7 +59,6 @@ export class JupyterDebugger implements IJupyterDebugger, ICellHashListener {
4759

4860
// Try to connect to this server
4961
const config = await this.connect(server);
50-
5162
if (config) {
5263
// First check if this is a live share session. Skip debugging attach on the guest
5364
// tslint:disable-next-line: no-any
@@ -111,25 +122,15 @@ export class JupyterDebugger implements IJupyterDebugger, ICellHashListener {
111122
}
112123
traceInfo('enable debugger attach');
113124

114-
// Current version of ptvsd doesn't support the source map entries, so we need to have a custom copy
115-
// on disk somewhere. Append this location to our sys path.
116-
// tslint:disable-next-line:no-multiline-string
117-
let extraPath = this.configService.getSettings().datascience.ptvsdDistPath;
118-
// Escape windows path chars so they end up in the source escaped
119-
if (this.platform.isWindows && extraPath) {
120-
extraPath = extraPath.replace('\\', '\\\\');
121-
}
122-
if (extraPath) {
123-
traceInfo(`Adding path for ptvsd - ${extraPath}`);
124-
await this.executeSilently(server, `import sys\r\nsys.path.append('${extraPath}')\r\nsys.path`);
125-
}
125+
// Append any specific ptvsd paths that we have
126+
await this.appendPtvsdPaths(server);
126127

127-
// Make sure we can use ptvsd
128-
const importResults = await this.executeSilently(server, 'import ptvsd');
129-
if (importResults && importResults.length > 0) {
130-
if (importResults[0].state === CellState.error) {
131-
throw new JupyterDebuggerNotInstalledError();
132-
}
128+
// Check the version of ptvsd that we have already installed
129+
const ptvsdVersion = await this.ptvsdCheck(server);
130+
131+
// If we don't have ptvsd installed or the version is too old then we need to install it
132+
if (!ptvsdVersion || !this.ptvsdMeetsRequirement(ptvsdVersion)) {
133+
await this.promptToInstallPtvsd(server, ptvsdVersion);
133134
}
134135

135136
// Connect local or remote based on what type of server we're talking to
@@ -147,6 +148,47 @@ export class JupyterDebugger implements IJupyterDebugger, ICellHashListener {
147148
return result;
148149
}
149150

151+
// Append our local ptvsd path and ptvsd settings path to sys.path
152+
private async appendPtvsdPaths(server: INotebookServer): Promise<void> {
153+
const extraPaths: string[] = [];
154+
155+
// Add the settings path first as it takes precedence over the ptvsd extension path
156+
// tslint:disable-next-line:no-multiline-string
157+
let settingsPath = this.configService.getSettings().datascience.ptvsdDistPath;
158+
// Escape windows path chars so they end up in the source escaped
159+
if (settingsPath) {
160+
if (this.platform.isWindows) {
161+
settingsPath = settingsPath.replace('\\', '\\\\');
162+
}
163+
164+
extraPaths.push(settingsPath);
165+
}
166+
167+
// For a local connection we also need will append on the path to the ptvsd
168+
// installed locally by the extension
169+
const connectionInfo = server.getConnectionInfo();
170+
if (connectionInfo && connectionInfo.localLaunch) {
171+
let localPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'lib', 'python');
172+
if (this.platform.isWindows) {
173+
localPath = localPath.replace('\\', '\\\\');
174+
}
175+
extraPaths.push(localPath);
176+
}
177+
178+
if (extraPaths && extraPaths.length > 0) {
179+
const pythonPathList = extraPaths.reduce((totalPath, currentPath) => {
180+
if (totalPath.length === 0) {
181+
totalPath = `'${currentPath}'`;
182+
} else {
183+
totalPath = `${totalPath}, '${currentPath}'`;
184+
}
185+
186+
return totalPath;
187+
}, '');
188+
await this.executeSilently(server, `import sys\r\nsys.path.extend([${pythonPathList}])\r\nsys.path`);
189+
}
190+
}
191+
150192
private buildSourceMap(fileHash: IFileHashes): ISourceMapRequest {
151193
const sourceMapRequest: ISourceMapRequest = { source: { path: fileHash.file }, pydevdSourceMaps: [] };
152194

@@ -166,6 +208,81 @@ export class JupyterDebugger implements IJupyterDebugger, ICellHashListener {
166208
return server.execute(code, Identifiers.EmptyFileName, 0, uuid(), undefined, true);
167209
}
168210

211+
// Returns either the version of ptvsd installed or undefined if not installed
212+
private async ptvsdCheck(server: INotebookServer): Promise<IPtvsdVersion | undefined> {
213+
// tslint:disable-next-line:no-multiline-string
214+
const ptvsdVersionResults = await this.executeSilently(server, `import ptvsd\r\nptvsd.__version__`);
215+
return this.parsePtvsdVersionInfo(ptvsdVersionResults);
216+
}
217+
218+
private parsePtvsdVersionInfo(cells: ICell[]): IPtvsdVersion | undefined {
219+
if (cells.length < 1 || cells[0].state !== CellState.finished) {
220+
return undefined;
221+
}
222+
223+
const targetCell = cells[0];
224+
225+
const outputString = this.extractOutput(targetCell);
226+
227+
if (outputString) {
228+
// Pull out the version number, note that we can't use SemVer here as python packages don't follow it
229+
const packageVersionRegex = /'([0-9]+).([0-9]+).([0-9a-zA-Z]+)/;
230+
const packageVersionMatch = packageVersionRegex.exec(outputString);
231+
232+
if (packageVersionMatch) {
233+
return {
234+
major: parseInt(packageVersionMatch[1], 10), minor: parseInt(packageVersionMatch[2], 10), revision: packageVersionMatch[3]
235+
};
236+
}
237+
}
238+
239+
return undefined;
240+
}
241+
242+
// Check to see if the we have the required version of ptvsd to support debugging
243+
private ptvsdMeetsRequirement(version: IPtvsdVersion): boolean {
244+
if (version.major > this.requiredPtvsdVersion.major) {
245+
return true;
246+
} else if (version.major === this.requiredPtvsdVersion.major && version.minor >= this.requiredPtvsdVersion.minor) {
247+
return true;
248+
}
249+
250+
return false;
251+
}
252+
253+
@captureTelemetry(Telemetry.PtvsdPromptToInstall)
254+
private async promptToInstallPtvsd(server: INotebookServer, oldVersion: IPtvsdVersion | undefined): Promise<void> {
255+
const promptMessage = oldVersion ? localize.DataScience.jupyterDebuggerInstallPtvsdUpdate() : localize.DataScience.jupyterDebuggerInstallPtvsdNew();
256+
const result = await this.appShell.showInformationMessage(promptMessage, localize.DataScience.jupyterDebuggerInstallPtvsdYes(), localize.DataScience.jupyterDebuggerInstallPtvsdNo());
257+
258+
if (result === localize.DataScience.jupyterDebuggerInstallPtvsdYes()) {
259+
await this.installPtvsd(server);
260+
} else {
261+
// If they don't want to install, throw so we exit out of debugging
262+
throw new JupyterDebuggerNotInstalledError();
263+
}
264+
}
265+
266+
private async installPtvsd(server: INotebookServer): Promise<void> {
267+
// tslint:disable-next-line:no-multiline-string
268+
const ptvsdInstallResults = await this.executeSilently(server, `!pip install ptvsd==v4.3.0b1`);
269+
270+
if (ptvsdInstallResults.length > 0) {
271+
const installResultsString = this.extractOutput(ptvsdInstallResults[0]);
272+
273+
if (installResultsString && installResultsString.includes('Successfully installed')) {
274+
sendTelemetryEvent(Telemetry.PtvsdSuccessfullyInstalled);
275+
traceInfo('Ptvsd successfully installed');
276+
return;
277+
}
278+
}
279+
280+
sendTelemetryEvent(Telemetry.PtvsdInstallFailed);
281+
traceError('Failed to install ptvsd');
282+
// Failed to install ptvsd, throw to exit debugging
283+
throw new JupyterDebuggerNotInstalledError();
284+
}
285+
169286
// Pull our connection info out from the cells returned by enable_attach
170287
private parseConnectInfo(cells: ICell[], local: boolean): DebugConfiguration | undefined {
171288
if (cells.length > 0) {
@@ -226,7 +343,7 @@ export class JupyterDebugger implements IJupyterDebugger, ICellHashListener {
226343

227344
private async connectToLocal(server: INotebookServer): Promise<DebugConfiguration | undefined> {
228345
// tslint:disable-next-line: no-multiline-string
229-
const enableDebuggerResults = await this.executeSilently(server, `ptvsd.enable_attach(('localhost', 0))`);
346+
const enableDebuggerResults = await this.executeSilently(server, `import ptvsd\r\nptvsd.enable_attach(('localhost', 0))`);
230347

231348
// Save our connection info to this server
232349
return this.parseConnectInfo(enableDebuggerResults, true);
@@ -241,10 +358,12 @@ export class JupyterDebugger implements IJupyterDebugger, ICellHashListener {
241358
// Loop through a bunch of ports until we find one we can use. Note how we
242359
// are connecting to '0.0.0.0' here. That's the location as far as ptvsd is concerned.
243360
const attachCode = portNumber !== -1 ?
244-
`ptvsd.enable_attach(('0.0.0.0', ${portNumber}))
361+
`import ptvsd
362+
ptvsd.enable_attach(('0.0.0.0', ${portNumber}))
245363
print("('${connectionInfo.hostName}', ${portNumber})")` :
246364
// tslint:disable-next-line: no-multiline-string
247-
`port = ${Settings.RemoteDebuggerPortBegin}
365+
`import ptvsd
366+
port = ${Settings.RemoteDebuggerPortBegin}
248367
attached = False
249368
while not attached and port <= ${Settings.RemoteDebuggerPortEnd}:
250369
try:

src/client/telemetry/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,9 @@ export interface IEventNamePropertyMapping {
372372
[Telemetry.InterruptJupyterTime]: never | undefined;
373373
[Telemetry.PandasNotInstalled]: never | undefined;
374374
[Telemetry.PandasTooOld]: never | undefined;
375+
[Telemetry.PtvsdInstallFailed]: never | undefined;
376+
[Telemetry.PtvsdPromptToInstall]: never | undefined;
377+
[Telemetry.PtvsdSuccessfullyInstalled]: never | undefined;
375378
[Telemetry.OpenPlotViewer]: never | undefined;
376379
[Telemetry.Redo]: never | undefined;
377380
[Telemetry.RemoteAddCode]: never | undefined;

0 commit comments

Comments
 (0)