11// Copyright (c) Microsoft Corporation. All rights reserved.
22// Licensed under the MIT License.
33'use strict' ;
4+ import '../common/extensions' ;
45
56import { inject , injectable } from 'inversify' ;
67import * as tk from 'tree-kill' ;
78import { URL } from 'url' ;
89
10+ import { IFileSystem } from '../common/platform/types' ;
911import { ExecutionResult , IPythonExecutionFactory , ObservableExecutionResult , Output , PythonVersionInfo } from '../common/process/types' ;
10- import { ILogger } from '../common/types' ;
12+ import { IConfigurationService , ILogger } from '../common/types' ;
1113import { createDeferred , Deferred } from '../common/utils/async' ;
12- import { IJupyterExecution , INotebookProcess } from './types' ;
14+ import * as localize from '../common/utils/localize' ;
15+ import { IJupyterExecution , INotebookProcess , JupyterServerInfo } from './types' ;
1316
1417export interface IConnectionInfo {
1518 baseUrl : string ;
@@ -19,20 +22,25 @@ export interface IConnectionInfo {
1922// This class communicates with an instance of jupyter that's running in the background
2023@injectable ( )
2124export class JupyterProcess implements INotebookProcess {
22- private static urlPattern = / h t t p : \/ \/ l o c a l h o s t : [ 0 - 9 ] + \/ \? t o k e n = [ a - z 0 - 9 ] + / ;
25+ private static urlPattern = / ( h t t p s ? : \/ \/ [ ^ \s ] + ) / ;
2326 private static forbiddenPattern = / F o r b i d d e n / ;
27+ private static httpPattern = / h t t p s ? : \/ \/ / ;
2428 public isDisposed : boolean = false ;
2529 private startPromise : Deferred < IConnectionInfo > | undefined ;
2630 private startObservable : ObservableExecutionResult < string > | undefined ;
31+ private launchTimeout : NodeJS . Timer ;
32+
33+ private notebook_dir : string ;
2734
2835 constructor (
2936 @inject ( IPythonExecutionFactory ) private executionFactory : IPythonExecutionFactory ,
37+ @inject ( IConfigurationService ) private configService : IConfigurationService ,
3038 @inject ( IJupyterExecution ) private jupyterExecution : IJupyterExecution ,
39+ @inject ( IFileSystem ) private fileSystem : IFileSystem ,
3140 @inject ( ILogger ) private logger : ILogger ) {
3241 }
3342
3443 public start = async ( notebookdir : string ) : Promise < void > => {
35-
3644 // Compute args based on if inside a workspace or not
3745 const args : string [ ] = [ 'notebook' , '--no-browser' , `--notebook-dir=${ notebookdir } ` ] ;
3846
@@ -41,6 +49,19 @@ export class JupyterProcess implements INotebookProcess {
4149
4250 // Use the IPythonExecutionService to find Jupyter
4351 this . startObservable = await this . jupyterExecution . execModuleObservable ( 'jupyter' , args , { throwOnStdErr : false , encoding : 'utf8' } ) ;
52+ // Save our notebook dir as we will use this to verify when we started up
53+ this . notebook_dir = notebookdir ;
54+
55+ // We want to reject our Jupyter connection after a specific timeout
56+ const settings = this . configService . getSettings ( ) ;
57+ const jupyterLaunchTimeout = settings . datascience . jupyterLaunchTimeout ;
58+
59+ if ( this . launchTimeout ) {
60+ clearTimeout ( this . launchTimeout ) ;
61+ }
62+ this . launchTimeout = setTimeout ( ( ) => {
63+ this . launchTimedOut ( ) ;
64+ } , jupyterLaunchTimeout ) ;
4465
4566 // Listen on stderr for its connection information
4667 this . startObservable . out . subscribe ( ( output : Output < string > ) => {
@@ -53,6 +74,7 @@ export class JupyterProcess implements INotebookProcess {
5374 }
5475
5576 public shutdown = async ( ) : Promise < void > => {
77+ clearTimeout ( this . launchTimeout ) ;
5678 if ( this . startObservable && this . startObservable . proc ) {
5779 if ( ! this . startObservable . proc . killed ) {
5880 tk ( this . startObservable . proc . pid ) ;
@@ -62,7 +84,6 @@ export class JupyterProcess implements INotebookProcess {
6284 }
6385
6486 public spawn = async ( notebookFile : string ) : Promise < ExecutionResult < string > > => {
65-
6687 // Compute args for the file
6788 const args : string [ ] = [ 'notebook' , `--NotebookApp.file_to_run=${ notebookFile } ` ] ;
6889
@@ -94,6 +115,7 @@ export class JupyterProcess implements INotebookProcess {
94115 return this . startPromise ! . promise ;
95116 }
96117
118+ clearTimeout ( this . launchTimeout ) ;
97119 return Promise . resolve ( { baseUrl : '' , token : '' } ) ;
98120 }
99121
@@ -111,25 +133,79 @@ export class JupyterProcess implements INotebookProcess {
111133 }
112134 }
113135
136+ // From a list of jupyter server infos try to find the matching jupyter that we launched
114137 // tslint:disable-next-line:no-any
115- private extractConnectionInformation = ( data : any ) => {
116- this . output ( data ) ;
138+ private getJupyterURL ( serverInfos : JupyterServerInfo [ ] , data : any ) {
139+ if ( serverInfos && ! this . startPromise . completed ) {
140+ const matchInfo = serverInfos . find ( info => this . fileSystem . arePathsSame ( this . notebook_dir , info [ 'notebook_dir' ] ) ) ;
141+ if ( matchInfo ) {
142+ const url = matchInfo [ 'url' ] ;
143+ const token = matchInfo [ 'token' ] ;
144+
145+ this . resolveStartPromise ( { baseUrl : url , token : token } ) ;
146+ }
147+ }
117148
118- // Look for a Jupyter Notebook url in the string received.
149+ // At this point we failed to get the server info or a matching server via the python code, so fall back to
150+ // our URL parse
151+ if ( ! this . startPromise . completed ) {
152+ this . getJupyterURLFromString ( data ) ;
153+ }
154+ }
155+
156+ // tslint:disable-next-line:no-any
157+ private getJupyterURLFromString ( data : any ) {
119158 const urlMatch = JupyterProcess . urlPattern . exec ( data ) ;
159+ if ( urlMatch && ! this . startPromise . completed ) {
160+ let url : URL ;
161+ try {
162+ url = new URL ( urlMatch [ 0 ] ) ;
163+ } catch ( err ) {
164+ // Failed to parse the url either via server infos or the string
165+ this . rejectStartPromise ( new Error ( localize . DataScience . jupyterLaunchNoURL ( ) ) ) ;
166+ return ;
167+ }
120168
121- if ( urlMatch && this . startPromise ) {
122- const url = new URL ( urlMatch [ 0 ] ) ;
123- this . startPromise . resolve ( { baseUrl : `${ url . protocol } //${ url . host } /` , token : `${ url . searchParams . get ( 'token' ) } ` } ) ;
169+ // Here we parsed the URL correctly
170+ this . resolveStartPromise ( { baseUrl : `${ url . protocol } //${ url . host } ${ url . pathname } ` , token : `${ url . searchParams . get ( 'token' ) } ` } ) ;
124171 }
172+ }
173+
174+ // tslint:disable-next-line:no-any
175+ private extractConnectionInformation = ( data : any ) => {
176+ this . output ( data ) ;
177+
178+ const httpMatch = JupyterProcess . httpPattern . exec ( data ) ;
125179
126- // Do we need to worry about this not working? Timeout?
180+ if ( httpMatch && this . notebook_dir && this . startPromise && ! this . startPromise . completed ) {
181+ // .then so that we can keep from pushing aync up to the subscribed observable function
182+ this . jupyterExecution . getJupyterServerInfo ( ) . then ( serverInfos => {
183+ this . getJupyterURL ( serverInfos , data ) ;
184+ } ) . ignoreErrors ( ) ;
185+ }
127186
128187 // Look for 'Forbidden' in the result
129188 const forbiddenMatch = JupyterProcess . forbiddenPattern . exec ( data ) ;
130189 if ( forbiddenMatch && this . startPromise && ! this . startPromise . resolved ) {
131- this . startPromise . reject ( new Error ( data . toString ( 'utf8' ) ) ) ;
190+ this . rejectStartPromise ( new Error ( data . toString ( 'utf8' ) ) ) ;
132191 }
192+ }
133193
194+ private launchTimedOut = ( ) => {
195+ if ( ! this . startPromise . completed ) {
196+ this . rejectStartPromise ( new Error ( localize . DataScience . jupyterLaunchTimedOut ( ) ) ) ;
197+ }
198+ }
199+
200+ private resolveStartPromise = ( value ?: IConnectionInfo | PromiseLike < IConnectionInfo > ) => {
201+ clearTimeout ( this . launchTimeout ) ;
202+ this . startPromise . resolve ( value ) ;
134203 }
204+
205+ // tslint:disable-next-line:no-any
206+ private rejectStartPromise = ( reason ?: any ) => {
207+ clearTimeout ( this . launchTimeout ) ;
208+ this . startPromise . reject ( reason ) ;
209+ }
210+
135211}
0 commit comments