Skip to content

launch JupyterLab server via a script to activate the environment properly #404

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Feb 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"cwd": "${workspaceFolder}",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
"runtimeExecutable": "${workspaceFolder}\\node_modules\\.bin\\electron.cmd"
},
"args" : ["."],
"outputCapture": "std"
Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,7 @@
},
"win": {
"target": [
"nsis",
"portable"
"nsis"
],
"extraFiles": [
{
Expand Down
4 changes: 2 additions & 2 deletions src/main/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ export class Registry implements IRegistry {
const envName = `${envInfo.type}: ${envInfo.name}`;

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

return path_env;
Expand Down
168 changes: 128 additions & 40 deletions src/main/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,120 @@ import {
import log from 'electron-log';

import * as fs from 'fs';
import { IPythonEnvironment } from './tokens';
import * as os from 'os';
import * as path from 'path';
import * as http from 'http';
import { IEnvironmentType, IPythonEnvironment } from './tokens';
import { appConfig } from './utils';

const SERVER_LAUNCH_TIMEOUT = 30000; // milliseconds

function createTempFile(fileName = 'temp', data = '', encoding: BufferEncoding = 'utf8') {
const tempDirPath = path.join(os.tmpdir(), 'jlab_desktop');
const tmpDir = fs.mkdtempSync(tempDirPath);
const tmpFilePath = path.join(tmpDir, fileName);

fs.writeFileSync(tmpFilePath, data, {encoding});

return tmpFilePath;
}

function createLaunchScript(environment: IPythonEnvironment): string {
const platform = process.platform;
const isWin = platform === 'win32';
const pythonPath = environment.path;
let envPath = path.dirname(pythonPath);
if (!isWin) {
envPath = path.normalize(path.join(envPath, '../'));
}

// note: traitlets<5.0 require fully specified arguments to
// be followed by equals sign without a space; this can be
// removed once jupyter_server requires traitlets>5.0
const launchCmd = [
'python', '-m', 'jupyterlab',
'--no-browser',
// do not use any config file
'--JupyterApp.config_file_name=""',
`--ServerApp.port=${appConfig.jlabPort}`,
// use our token rather than any pre-configured password
'--ServerApp.password=""',
'--ServerApp.allow_origin="*"',
// enable hidden files (let user decide whether to display them)
'--ContentsManager.allow_hidden=True'
].join(' ');

let script: string;

if (isWin) {
if (environment.type === IEnvironmentType.CondaEnv) {
script = `
CALL ${envPath}\\condabin\\activate.bat
CALL ${launchCmd}`;
} else {
script = `
CALL ${envPath}\\activate.bat
CALL ${launchCmd}`;
}
} else {
script = `
source ${envPath}/bin/activate
${launchCmd}`;
}

const ext = isWin ? 'bat' : 'sh';
const scriptPath = createTempFile(`launch.${ext}`, script);

if (!isWin) {
fs.chmodSync(scriptPath, 0o755);
}

return scriptPath;
}

async function waitForDuration(duration: number): Promise<boolean> {
return new Promise(resolve => {
setTimeout(() => {
resolve(false);
}, duration);
});
}

async function checkIfUrlExists(url: URL): Promise<boolean> {
return new Promise<boolean>((resolve) => {
const req = http.request(url, function(r) {
resolve(r.statusCode >= 200 && r.statusCode < 400);
});
req.on('error', function(err) {
resolve(false);
});
req.end();
});
}

async function waitUntilServerIsUp(port: number): Promise<boolean> {
const url = new URL(`http://localhost:${port}`);
return new Promise<boolean>(async (resolve) => {
async function checkUrl() {
const exists = await checkIfUrlExists(url);
if (exists) {
return resolve(true);
} else {
setTimeout(async () => {
await checkUrl();
}, 500);
}
}

await checkUrl();
});
}

export
class JupyterServer {

constructor(options: JupyterServer.IOptions, registry: IRegistry) {
constructor(options: JupyterServer.IOptions) {
this._info.environment = options.environment;
this._registry = registry;
}

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

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

const launchScriptPath = createLaunchScript(this._info.environment);

// note: traitlets<5.0 require fully specified arguments to
// be followed by equals sign without a space; this can be
// removed once jupyter_server requires traitlets>5.0
this._nbServer = execFile(this._info.environment.path, [
'-m', 'jupyterlab',
'--no-browser',
// do not use any config file
'--JupyterApp.config_file_name=""',
`--ServerApp.port=${appConfig.jlabPort}`,
// use our token rather than any pre-configured password
'--ServerApp.password=""',
'--ServerApp.allow_origin="*"',
// enable hidden files (let user decide whether to display them)
'--ContentsManager.allow_hidden=True'
], {
this._nbServer = execFile(launchScriptPath, {
cwd: home,
shell: isWin ? 'cmd.exe' : '/bin/bash',
env: {
...process.env,
PATH: this._registry.getAdditionalPathIncludesForPythonPath(this._info.environment.path),
JUPYTER_TOKEN: appConfig.token,
JUPYTER_CONFIG_DIR: process.env.JLAB_DESKTOP_CONFIG_DIR || app.getPath('userData')
}
});

Promise.race([
waitUntilServerIsUp(appConfig.jlabPort),
waitForDuration(SERVER_LAUNCH_TIMEOUT)
])
.then((up: boolean) => {
this._cleanupListeners();
if (up) {
fs.unlinkSync(launchScriptPath);
resolve(this._info);
} else {
reject(new Error('Failed to launch Jupyter Server'));
}
});

this._nbServer.on('exit', () => {
if (started) {
Expand All @@ -109,22 +215,6 @@ class JupyterServer {
reject(err);
}
});

this._nbServer.stderr.on('data', (serverBuff: string | Buffer) => {
const line = serverBuff.toString();
let urlMatch = line.match(urlRegExp);
let versionMatch = serverVersionPattern.exec(line);

if (versionMatch) {
this._info.version = versionMatch.groups.version;
}
if (urlMatch) {
started = true;
this._cleanupListeners();
return resolve(this._info);
}
console.log('Jupyter Server initialization message:', serverBuff);
});
});

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

private _info: JupyterServer.IInfo = { url: null, token: null, environment: null, version: null };

private _registry: IRegistry;
}

export
Expand Down Expand Up @@ -447,7 +535,7 @@ class JupyterServerFactory implements IServerFactory, IClosingService {
private _createServer(opts: JupyterServer.IOptions): JupyterServerFactory.IFactoryItem {
let item: JupyterServerFactory.IFactoryItem = {
factoryId: this._nextId++,
server: new JupyterServer(opts, this._registry),
server: new JupyterServer(opts),
closing: null,
used: false
};
Expand Down