44import  {  nbformat  }  from  '@jupyterlab/coreutils' ; 
55import  {  inject ,  injectable  }  from  'inversify' ; 
66import  *  as  net  from  'net' ; 
7+ import  *  as  path  from  'path' ; 
78import  *  as  uuid  from  'uuid/v4' ; 
89import  {  DebugConfiguration  }  from  'vscode' ; 
910import  *  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' ; 
1314import  {  IPlatformService  }  from  '../../common/platform/types' ; 
1415import  {  IConfigurationService  }  from  '../../common/types' ; 
1516import  {  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' ; 
1620import  {  concatMultilineString  }  from  '../common' ; 
17- import  {  Identifiers ,  Settings  }  from  '../constants' ; 
21+ import  {  Identifiers ,  Settings ,   Telemetry  }  from  '../constants' ; 
1822import  { 
1923 CellState , 
2024 ICell , 
@@ -30,10 +34,18 @@ import { JupyterDebuggerPortBlockedError } from './jupyterDebuggerPortBlockedErr
3034import  {  JupyterDebuggerPortNotAvailableError  }  from  './jupyterDebuggerPortNotAvailableError' ; 
3135import  {  ILiveShareHasRole  }  from  './liveshare/types' ; 
3236
37+ interface  IPtvsdVersion  { 
38+  major : number ; 
39+  minor : number ; 
40+  revision : string ; 
41+ } 
42+ 
3343@injectable ( ) 
3444export  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 }  ) ; 
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 }  ) ; 
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 - 9 a - z A - 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 }  
245363print("('${ connectionInfo . hostName } ${ portNumber }   :
246364 // tslint:disable-next-line: no-multiline-string 
247-  `port = ${ Settings . RemoteDebuggerPortBegin }  
365+  `import ptvsd 
366+ port = ${ Settings . RemoteDebuggerPortBegin }  
248367attached = False 
249368while not attached and port <= ${ Settings . RemoteDebuggerPortEnd }  
250369 try: 
0 commit comments