@@ -29,15 +29,120 @@ import {
29
29
import log from 'electron-log' ;
30
30
31
31
import * as fs from 'fs' ;
32
- import { IPythonEnvironment } from './tokens' ;
32
+ import * as os from 'os' ;
33
+ import * as path from 'path' ;
34
+ import * as http from 'http' ;
35
+ import { IEnvironmentType , IPythonEnvironment } from './tokens' ;
33
36
import { appConfig } from './utils' ;
34
37
38
+ const SERVER_LAUNCH_TIMEOUT = 30000 ; // milliseconds
39
+
40
+ function createTempFile ( fileName = 'temp' , data = '' , encoding : BufferEncoding = 'utf8' ) {
41
+ const tempDirPath = path . join ( os . tmpdir ( ) , 'jlab_desktop' ) ;
42
+ const tmpDir = fs . mkdtempSync ( tempDirPath ) ;
43
+ const tmpFilePath = path . join ( tmpDir , fileName ) ;
44
+
45
+ fs . writeFileSync ( tmpFilePath , data , { encoding} ) ;
46
+
47
+ return tmpFilePath ;
48
+ }
49
+
50
+ function createLaunchScript ( environment : IPythonEnvironment ) : string {
51
+ const platform = process . platform ;
52
+ const isWin = platform === 'win32' ;
53
+ const pythonPath = environment . path ;
54
+ let envPath = path . dirname ( pythonPath ) ;
55
+ if ( ! isWin ) {
56
+ envPath = path . normalize ( path . join ( envPath , '../' ) ) ;
57
+ }
58
+
59
+ // note: traitlets<5.0 require fully specified arguments to
60
+ // be followed by equals sign without a space; this can be
61
+ // removed once jupyter_server requires traitlets>5.0
62
+ const launchCmd = [
63
+ 'python' , '-m' , 'jupyterlab' ,
64
+ '--no-browser' ,
65
+ // do not use any config file
66
+ '--JupyterApp.config_file_name=""' ,
67
+ `--ServerApp.port=${ appConfig . jlabPort } ` ,
68
+ // use our token rather than any pre-configured password
69
+ '--ServerApp.password=""' ,
70
+ '--ServerApp.allow_origin="*"' ,
71
+ // enable hidden files (let user decide whether to display them)
72
+ '--ContentsManager.allow_hidden=True'
73
+ ] . join ( ' ' ) ;
74
+
75
+ let script : string ;
76
+
77
+ if ( isWin ) {
78
+ if ( environment . type === IEnvironmentType . CondaEnv ) {
79
+ script = `
80
+ CALL ${ envPath } \\condabin\\activate.bat
81
+ CALL ${ launchCmd } ` ;
82
+ } else {
83
+ script = `
84
+ CALL ${ envPath } \\activate.bat
85
+ CALL ${ launchCmd } ` ;
86
+ }
87
+ } else {
88
+ script = `
89
+ source ${ envPath } /bin/activate
90
+ ${ launchCmd } ` ;
91
+ }
92
+
93
+ const ext = isWin ? 'bat' : 'sh' ;
94
+ const scriptPath = createTempFile ( `launch.${ ext } ` , script ) ;
95
+
96
+ if ( ! isWin ) {
97
+ fs . chmodSync ( scriptPath , 0o755 ) ;
98
+ }
99
+
100
+ return scriptPath ;
101
+ }
102
+
103
+ async function waitForDuration ( duration : number ) : Promise < boolean > {
104
+ return new Promise ( resolve => {
105
+ setTimeout ( ( ) => {
106
+ resolve ( false ) ;
107
+ } , duration ) ;
108
+ } ) ;
109
+ }
110
+
111
+ async function checkIfUrlExists ( url : URL ) : Promise < boolean > {
112
+ return new Promise < boolean > ( ( resolve ) => {
113
+ const req = http . request ( url , function ( r ) {
114
+ resolve ( r . statusCode >= 200 && r . statusCode < 400 ) ;
115
+ } ) ;
116
+ req . on ( 'error' , function ( err ) {
117
+ resolve ( false ) ;
118
+ } ) ;
119
+ req . end ( ) ;
120
+ } ) ;
121
+ }
122
+
123
+ async function waitUntilServerIsUp ( port : number ) : Promise < boolean > {
124
+ const url = new URL ( `http://localhost:${ port } ` ) ;
125
+ return new Promise < boolean > ( async ( resolve ) => {
126
+ async function checkUrl ( ) {
127
+ const exists = await checkIfUrlExists ( url ) ;
128
+ if ( exists ) {
129
+ return resolve ( true ) ;
130
+ } else {
131
+ setTimeout ( async ( ) => {
132
+ await checkUrl ( ) ;
133
+ } , 500 ) ;
134
+ }
135
+ }
136
+
137
+ await checkUrl ( ) ;
138
+ } ) ;
139
+ }
140
+
35
141
export
36
142
class JupyterServer {
37
143
38
- constructor ( options : JupyterServer . IOptions , registry : IRegistry ) {
144
+ constructor ( options : JupyterServer . IOptions ) {
39
145
this . _info . environment = options . environment ;
40
- this . _registry = registry ;
41
146
}
42
147
43
148
get info ( ) : JupyterServer . IInfo {
@@ -57,40 +162,41 @@ class JupyterServer {
57
162
let started = false ;
58
163
59
164
this . _startServer = new Promise < JupyterServer . IInfo > ( ( resolve , reject ) => {
60
- let urlRegExp = / h t t p s ? : \/ \/ l o c a l h o s t : \d + \/ \S * / g;
61
- let serverVersionPattern = / J u p y t e r S e r v e r (?< version > .* ) i s r u n n i n g a t / g;
62
165
const home = process . env . JLAB_DESKTOP_HOME || app . getPath ( 'home' ) ;
166
+ const isWin = process . platform === 'win32' ;
63
167
const pythonPath = this . _info . environment . path ;
64
168
if ( ! fs . existsSync ( pythonPath ) ) {
65
169
dialog . showMessageBox ( { message : `Environment not found at: ${ pythonPath } ` , type : 'error' } ) ;
66
170
reject ( ) ;
67
171
}
68
172
this . _info . url = `http://localhost:${ appConfig . jlabPort } ` ;
69
173
this . _info . token = appConfig . token ;
174
+
175
+ const launchScriptPath = createLaunchScript ( this . _info . environment ) ;
70
176
71
- // note: traitlets<5.0 require fully specified arguments to
72
- // be followed by equals sign without a space; this can be
73
- // removed once jupyter_server requires traitlets>5.0
74
- this . _nbServer = execFile ( this . _info . environment . path , [
75
- '-m' , 'jupyterlab' ,
76
- '--no-browser' ,
77
- // do not use any config file
78
- '--JupyterApp.config_file_name=""' ,
79
- `--ServerApp.port=${ appConfig . jlabPort } ` ,
80
- // use our token rather than any pre-configured password
81
- '--ServerApp.password=""' ,
82
- '--ServerApp.allow_origin="*"' ,
83
- // enable hidden files (let user decide whether to display them)
84
- '--ContentsManager.allow_hidden=True'
85
- ] , {
177
+ this . _nbServer = execFile ( launchScriptPath , {
86
178
cwd : home ,
179
+ shell : isWin ? 'cmd.exe' : '/bin/bash' ,
87
180
env : {
88
181
...process . env ,
89
- PATH : this . _registry . getAdditionalPathIncludesForPythonPath ( this . _info . environment . path ) ,
90
182
JUPYTER_TOKEN : appConfig . token ,
91
183
JUPYTER_CONFIG_DIR : process . env . JLAB_DESKTOP_CONFIG_DIR || app . getPath ( 'userData' )
92
184
}
93
185
} ) ;
186
+
187
+ Promise . race ( [
188
+ waitUntilServerIsUp ( appConfig . jlabPort ) ,
189
+ waitForDuration ( SERVER_LAUNCH_TIMEOUT )
190
+ ] )
191
+ . then ( ( up : boolean ) => {
192
+ this . _cleanupListeners ( ) ;
193
+ if ( up ) {
194
+ fs . unlinkSync ( launchScriptPath ) ;
195
+ resolve ( this . _info ) ;
196
+ } else {
197
+ reject ( new Error ( 'Failed to launch Jupyter Server' ) ) ;
198
+ }
199
+ } ) ;
94
200
95
201
this . _nbServer . on ( 'exit' , ( ) => {
96
202
if ( started ) {
@@ -109,22 +215,6 @@ class JupyterServer {
109
215
reject ( err ) ;
110
216
}
111
217
} ) ;
112
-
113
- this . _nbServer . stderr . on ( 'data' , ( serverBuff : string | Buffer ) => {
114
- const line = serverBuff . toString ( ) ;
115
- let urlMatch = line . match ( urlRegExp ) ;
116
- let versionMatch = serverVersionPattern . exec ( line ) ;
117
-
118
- if ( versionMatch ) {
119
- this . _info . version = versionMatch . groups . version ;
120
- }
121
- if ( urlMatch ) {
122
- started = true ;
123
- this . _cleanupListeners ( ) ;
124
- return resolve ( this . _info ) ;
125
- }
126
- console . log ( 'Jupyter Server initialization message:' , serverBuff ) ;
127
- } ) ;
128
218
} ) ;
129
219
130
220
return this . _startServer ;
@@ -179,8 +269,6 @@ class JupyterServer {
179
269
private _startServer : Promise < JupyterServer . IInfo > = null ;
180
270
181
271
private _info : JupyterServer . IInfo = { url : null , token : null , environment : null , version : null } ;
182
-
183
- private _registry : IRegistry ;
184
272
}
185
273
186
274
export
@@ -447,7 +535,7 @@ class JupyterServerFactory implements IServerFactory, IClosingService {
447
535
private _createServer ( opts : JupyterServer . IOptions ) : JupyterServerFactory . IFactoryItem {
448
536
let item : JupyterServerFactory . IFactoryItem = {
449
537
factoryId : this . _nextId ++ ,
450
- server : new JupyterServer ( opts , this . _registry ) ,
538
+ server : new JupyterServer ( opts ) ,
451
539
closing : null ,
452
540
used : false
453
541
} ;
0 commit comments