Skip to content

Commit 1fc6218

Browse files
authored
Merge pull request #404 from mbektas/server_launch_script
launch JupyterLab server via a script to activate the environment properly
2 parents 6980447 + 38c4455 commit 1fc6218

File tree

4 files changed

+132
-45
lines changed

4 files changed

+132
-45
lines changed

.vscode/launch.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"cwd": "${workspaceFolder}",
2222
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
2323
"windows": {
24-
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
24+
"runtimeExecutable": "${workspaceFolder}\\node_modules\\.bin\\electron.cmd"
2525
},
2626
"args" : ["."],
2727
"outputCapture": "std"

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,7 @@
6868
},
6969
"win": {
7070
"target": [
71-
"nsis",
72-
"portable"
71+
"nsis"
7372
],
7473
"extraFiles": [
7574
{

src/main/registry.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ export class Registry implements IRegistry {
277277
const envName = `${envInfo.type}: ${envInfo.name}`;
278278

279279
return {
280-
type: IEnvironmentType.PATH,
280+
type: envInfo.type === 'conda' ? IEnvironmentType.CondaEnv : IEnvironmentType.VirtualEnv,
281281
name: envName,
282282
path: pythonPath,
283283
versions: { 'python': pythonVersion, 'jupyterlab': jlabVersion },
@@ -316,7 +316,7 @@ export class Registry implements IRegistry {
316316
if (platform === 'win32') {
317317
path_env = `${envPath};${envPath}\\Library\\mingw-w64\\bin;${envPath}\\Library\\usr\\bin;${envPath}\\Library\\bin;${envPath}\\Scripts;${envPath}\\bin;${process.env['PATH']}`;
318318
} else {
319-
path_env = `${envPath}:${process.env['PATH']}`;
319+
path_env = `${envPath}:${envPath}/bin:${process.env['PATH']}`;
320320
}
321321

322322
return path_env;

src/main/server.ts

Lines changed: 128 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,120 @@ import {
2929
import log from 'electron-log';
3030

3131
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';
3336
import { appConfig } from './utils';
3437

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+
35141
export
36142
class JupyterServer {
37143

38-
constructor(options: JupyterServer.IOptions, registry: IRegistry) {
144+
constructor(options: JupyterServer.IOptions) {
39145
this._info.environment = options.environment;
40-
this._registry = registry;
41146
}
42147

43148
get info(): JupyterServer.IInfo {
@@ -57,40 +162,41 @@ class JupyterServer {
57162
let started = false;
58163

59164
this._startServer = new Promise<JupyterServer.IInfo>((resolve, reject) => {
60-
let urlRegExp = /https?:\/\/localhost:\d+\/\S*/g;
61-
let serverVersionPattern = /Jupyter Server (?<version>.*) is running at/g;
62165
const home = process.env.JLAB_DESKTOP_HOME || app.getPath('home');
166+
const isWin = process.platform === 'win32';
63167
const pythonPath = this._info.environment.path;
64168
if (!fs.existsSync(pythonPath)) {
65169
dialog.showMessageBox({message: `Environment not found at: ${pythonPath}`, type: 'error' });
66170
reject();
67171
}
68172
this._info.url = `http://localhost:${appConfig.jlabPort}`;
69173
this._info.token = appConfig.token;
174+
175+
const launchScriptPath = createLaunchScript(this._info.environment);
70176

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, {
86178
cwd: home,
179+
shell: isWin ? 'cmd.exe' : '/bin/bash',
87180
env: {
88181
...process.env,
89-
PATH: this._registry.getAdditionalPathIncludesForPythonPath(this._info.environment.path),
90182
JUPYTER_TOKEN: appConfig.token,
91183
JUPYTER_CONFIG_DIR: process.env.JLAB_DESKTOP_CONFIG_DIR || app.getPath('userData')
92184
}
93185
});
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+
});
94200

95201
this._nbServer.on('exit', () => {
96202
if (started) {
@@ -109,22 +215,6 @@ class JupyterServer {
109215
reject(err);
110216
}
111217
});
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-
});
128218
});
129219

130220
return this._startServer;
@@ -179,8 +269,6 @@ class JupyterServer {
179269
private _startServer: Promise<JupyterServer.IInfo> = null;
180270

181271
private _info: JupyterServer.IInfo = { url: null, token: null, environment: null, version: null };
182-
183-
private _registry: IRegistry;
184272
}
185273

186274
export
@@ -447,7 +535,7 @@ class JupyterServerFactory implements IServerFactory, IClosingService {
447535
private _createServer(opts: JupyterServer.IOptions): JupyterServerFactory.IFactoryItem {
448536
let item: JupyterServerFactory.IFactoryItem = {
449537
factoryId: this._nextId++,
450-
server: new JupyterServer(opts, this._registry),
538+
server: new JupyterServer(opts),
451539
closing: null,
452540
used: false
453541
};

0 commit comments

Comments
 (0)