mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Merge from master
This commit is contained in:
@@ -1,399 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as UUID from 'vscode-languageclient/lib/utils/uuid';
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as os from 'os';
|
||||
import { spawn, ExecOptions, SpawnOptions, ChildProcess } from 'child_process';
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
import { IServerInstance } from './common';
|
||||
import JupyterServerInstallation from './jupyterServerInstallation';
|
||||
import * as utils from '../common/utils';
|
||||
import * as constants from '../common/constants';
|
||||
import * as notebookUtils from '../common/notebookUtils';
|
||||
import * as ports from '../common/ports';
|
||||
|
||||
const NotebookConfigFilename = 'jupyter_notebook_config.py';
|
||||
const CustomJsFilename = 'custom.js';
|
||||
const defaultPort = 8888;
|
||||
const JupyterStartedMessage = 'The Jupyter Notebook is running';
|
||||
|
||||
type MessageListener = (data: string | Buffer) => void;
|
||||
type ErrorListener = (err: any) => void;
|
||||
|
||||
export interface IInstanceOptions {
|
||||
/**
|
||||
* The path to the initial document we want to start this server for
|
||||
*/
|
||||
documentPath: string;
|
||||
|
||||
/**
|
||||
* Base install information needed in order to start the server instance
|
||||
*/
|
||||
install: JupyterServerInstallation;
|
||||
|
||||
/**
|
||||
* Optional start directory for the notebook server. If none is set, will use a
|
||||
* path relative to the initial document
|
||||
*/
|
||||
notebookDirectory?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class to enable testing without calling into file system or
|
||||
* commandline shell APIs
|
||||
*/
|
||||
export class ServerInstanceUtils {
|
||||
public mkDir(dirPath: string, outputChannel?: vscode.OutputChannel): Promise<void> {
|
||||
return utils.mkDir(dirPath, outputChannel);
|
||||
}
|
||||
public removeDir(dirPath: string): Promise<void> {
|
||||
return fs.remove(dirPath);
|
||||
}
|
||||
public pathExists(dirPath: string): Promise<boolean> {
|
||||
return fs.pathExists(dirPath);
|
||||
}
|
||||
public copy(src: string, dest: string): Promise<void> {
|
||||
return fs.copy(src, dest);
|
||||
}
|
||||
public existsSync(dirPath: string): boolean {
|
||||
return fs.existsSync(dirPath);
|
||||
}
|
||||
public generateUuid(): string {
|
||||
return UUID.generateUuid();
|
||||
}
|
||||
public executeBufferedCommand(cmd: string, options: ExecOptions, outputChannel?: vscode.OutputChannel): Thenable<string> {
|
||||
return utils.executeBufferedCommand(cmd, options, outputChannel);
|
||||
}
|
||||
|
||||
public spawn(command: string, args?: ReadonlyArray<string>, options?: SpawnOptions): ChildProcess {
|
||||
return spawn(command, args, options);
|
||||
}
|
||||
|
||||
public checkProcessDied(childProcess: ChildProcess): void {
|
||||
if (!childProcess) {
|
||||
return;
|
||||
}
|
||||
// Wait 10 seconds and then force kill. Jupyter stop is slow so this seems a reasonable time limit
|
||||
setTimeout(() => {
|
||||
// Test if the process is still alive. Throws an exception if not
|
||||
try {
|
||||
process.kill(childProcess.pid, <any>0);
|
||||
} catch (error) {
|
||||
// All is fine.
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
}
|
||||
|
||||
export class PerNotebookServerInstance implements IServerInstance {
|
||||
|
||||
/**
|
||||
* Root of the jupyter directory structure. Config and data roots will be
|
||||
* under this, in order to simplify deletion of folders on stop of the instance
|
||||
*/
|
||||
private baseDir: string;
|
||||
|
||||
/**
|
||||
* Path to configuration folder for this instance. Typically:
|
||||
* %extension_path%/jupyter_config/%server%_config
|
||||
*/
|
||||
private instanceConfigRoot: string;
|
||||
|
||||
/**
|
||||
* Path to data folder for this instance. Typically:
|
||||
* %extension_path%/jupyter_config/%server%_data
|
||||
*/
|
||||
private instanceDataRoot: string;
|
||||
|
||||
private _systemJupyterDir: string;
|
||||
private _port: string;
|
||||
private _uri: vscode.Uri;
|
||||
private _isStarted: boolean = false;
|
||||
private utils: ServerInstanceUtils;
|
||||
private childProcess: ChildProcess;
|
||||
private errorHandler: ErrorHandler = new ErrorHandler();
|
||||
|
||||
constructor(private options: IInstanceOptions, fsUtils?: ServerInstanceUtils) {
|
||||
this.utils = fsUtils || new ServerInstanceUtils();
|
||||
}
|
||||
|
||||
public get isStarted(): boolean {
|
||||
return this._isStarted;
|
||||
}
|
||||
|
||||
public get port(): string {
|
||||
return this._port;
|
||||
}
|
||||
|
||||
public get uri(): vscode.Uri {
|
||||
return this._uri;
|
||||
}
|
||||
|
||||
public async configure(): Promise<void> {
|
||||
await this.configureJupyter();
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
await this.startInternal();
|
||||
}
|
||||
|
||||
public async stop(): Promise<void> {
|
||||
try {
|
||||
if (this.baseDir) {
|
||||
let exists = await this.utils.pathExists(this.baseDir);
|
||||
if (exists) {
|
||||
await this.utils.removeDir(this.baseDir);
|
||||
}
|
||||
}
|
||||
if (this.isStarted) {
|
||||
let install = this.options.install;
|
||||
let stopCommand = `${install.pythonExecutable} -m jupyter notebook stop ${this._port}`;
|
||||
await this.utils.executeBufferedCommand(stopCommand, install.execOptions, install.outputChannel);
|
||||
this._isStarted = false;
|
||||
this.utils.checkProcessDied(this.childProcess);
|
||||
this.handleConnectionClosed();
|
||||
}
|
||||
} catch (error) {
|
||||
// For now, we don't care as this is non-critical
|
||||
this.notify(this.options.install, localize('serverStopError', 'Error stopping Notebook Server: {0}', utils.getErrorMessage(error)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async configureJupyter(): Promise<void> {
|
||||
await this.createInstanceFolders();
|
||||
let resourcesFolder = path.join(this.options.install.extensionPath, 'resources', constants.jupyterConfigRootFolder);
|
||||
await this.copyInstanceConfig(resourcesFolder);
|
||||
await this.CopyCustomJs(resourcesFolder);
|
||||
await this.copyKernelsToSystemJupyterDirs();
|
||||
}
|
||||
|
||||
private async createInstanceFolders(): Promise<void> {
|
||||
this.baseDir = path.join(this.options.install.configRoot, 'instances', `${this.utils.generateUuid()}`);
|
||||
this.instanceConfigRoot = path.join(this.baseDir, 'config');
|
||||
this.instanceDataRoot = path.join(this.baseDir, 'data');
|
||||
await this.utils.mkDir(this.baseDir, this.options.install.outputChannel);
|
||||
await this.utils.mkDir(this.instanceConfigRoot, this.options.install.outputChannel);
|
||||
await this.utils.mkDir(this.instanceDataRoot, this.options.install.outputChannel);
|
||||
}
|
||||
|
||||
private async copyInstanceConfig(resourcesFolder: string): Promise<void> {
|
||||
let configSource = path.join(resourcesFolder, NotebookConfigFilename);
|
||||
let configDest = path.join(this.instanceConfigRoot, NotebookConfigFilename);
|
||||
await this.utils.copy(configSource, configDest);
|
||||
}
|
||||
|
||||
private async CopyCustomJs(resourcesFolder: string): Promise<void> {
|
||||
let customPath = path.join(this.instanceConfigRoot, 'custom');
|
||||
await this.utils.mkDir(customPath, this.options.install.outputChannel);
|
||||
let customSource = path.join(resourcesFolder, CustomJsFilename);
|
||||
let customDest = path.join(customPath, CustomJsFilename);
|
||||
await this.utils.copy(customSource, customDest);
|
||||
}
|
||||
|
||||
private async copyKernelsToSystemJupyterDirs(): Promise<void> {
|
||||
let kernelsExtensionSource = path.join(this.options.install.extensionPath, 'kernels');
|
||||
this._systemJupyterDir = this.getSystemJupyterKernelDir();
|
||||
if (!this.utils.existsSync(this._systemJupyterDir)) {
|
||||
await this.utils.mkDir(this._systemJupyterDir, this.options.install.outputChannel);
|
||||
}
|
||||
await this.utils.copy(kernelsExtensionSource, this._systemJupyterDir);
|
||||
}
|
||||
|
||||
private getSystemJupyterKernelDir(): string {
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
let appDataWindows = process.env['APPDATA'];
|
||||
return appDataWindows + '\\jupyter\\kernels';
|
||||
case 'darwin':
|
||||
return path.resolve(os.homedir(), 'Library/Jupyter/kernels');
|
||||
default:
|
||||
return path.resolve(os.homedir(), '.local/share/jupyter/kernels');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Starts a Jupyter instance using the provided a start command. Server is determined to have
|
||||
* started when the log message with URL to connect to is emitted.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
protected async startInternal(): Promise<void> {
|
||||
if (this.isStarted) {
|
||||
return;
|
||||
}
|
||||
let notebookDirectory = this.getNotebookDirectory();
|
||||
// Find a port in a given range. If run into trouble, got up 100 in range and search inside a larger range
|
||||
let port = await ports.strictFindFreePort(new ports.StrictPortFindOptions(defaultPort, defaultPort + 100, defaultPort + 1000));
|
||||
let token = await notebookUtils.getRandomToken();
|
||||
this._uri = vscode.Uri.parse(`http://localhost:${port}/?token=${token}`);
|
||||
this._port = port.toString();
|
||||
let startCommand = `${this.options.install.pythonExecutable} -m jupyter notebook --no-browser --notebook-dir "${notebookDirectory}" --port=${port} --NotebookApp.token=${token}`;
|
||||
this.notifyStarting(this.options.install, startCommand);
|
||||
|
||||
// Execute the command
|
||||
await this.executeStartCommand(startCommand);
|
||||
}
|
||||
|
||||
private executeStartCommand(startCommand: string): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
let install = this.options.install;
|
||||
this.childProcess = this.spawnJupyterProcess(install, startCommand);
|
||||
|
||||
// Add listeners for the process exiting prematurely
|
||||
let onErrorBeforeStartup = (err) => reject(err);
|
||||
let onExitBeforeStart = (err) => {
|
||||
if (!this.isStarted) {
|
||||
reject(localize('notebookStartProcessExitPremature', 'Notebook process exited prematurely with error: {0}', err));
|
||||
}
|
||||
};
|
||||
this.childProcess.on('error', onErrorBeforeStartup);
|
||||
this.childProcess.on('exit', onExitBeforeStart);
|
||||
|
||||
// Add listener for the process to emit its web address
|
||||
let handleStdout = (data: string | Buffer) => { install.outputChannel.appendLine(data.toString()); };
|
||||
let handleStdErr = (data: string | Buffer) => {
|
||||
// For some reason, URL info is sent on StdErr
|
||||
let [url, port] = this.matchUrlAndPort(data);
|
||||
if (url) {
|
||||
// For now, will verify port matches
|
||||
if (url.authority !== this._uri.authority
|
||||
|| url.query !== this._uri.query) {
|
||||
this._uri = url;
|
||||
this._port = port;
|
||||
}
|
||||
this.notifyStarted(install, url.toString());
|
||||
this._isStarted = true;
|
||||
|
||||
this.updateListeners(handleStdout, handleStdErr, onErrorBeforeStartup, onExitBeforeStart);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
this.childProcess.stdout.on('data', handleStdout);
|
||||
this.childProcess.stderr.on('data', handleStdErr);
|
||||
});
|
||||
}
|
||||
|
||||
private updateListeners(handleStdout: MessageListener, handleStdErr: MessageListener, onErrorBeforeStartup: ErrorListener, onExitBeforeStart: ErrorListener): void {
|
||||
this.childProcess.stdout.removeListener('data', handleStdout);
|
||||
this.childProcess.stderr.removeListener('data', handleStdErr);
|
||||
this.childProcess.removeListener('error', onErrorBeforeStartup);
|
||||
this.childProcess.removeListener('exit', onExitBeforeStart);
|
||||
|
||||
this.childProcess.addListener('error', this.handleConnectionError);
|
||||
this.childProcess.addListener('exit', this.handleConnectionClosed);
|
||||
|
||||
// TODO #897 covers serializing stdout and stderr to a location where we can read from so that user can see if they run into trouble
|
||||
}
|
||||
|
||||
private handleConnectionError(error: Error): void {
|
||||
let action = this.errorHandler.handleError(error);
|
||||
if (action === ErrorAction.Shutdown) {
|
||||
this.notify(this.options.install, localize('jupyterError', 'Error sent from Jupyter: {0}', utils.getErrorMessage(error)));
|
||||
this.stop();
|
||||
}
|
||||
}
|
||||
private handleConnectionClosed(): void {
|
||||
this.childProcess = undefined;
|
||||
this._isStarted = false;
|
||||
}
|
||||
|
||||
getNotebookDirectory(): string {
|
||||
if (this.options.notebookDirectory) {
|
||||
if (this.options.notebookDirectory.endsWith('\\')) {
|
||||
return this.options.notebookDirectory.substr(0, this.options.notebookDirectory.length - 1) + '/';
|
||||
}
|
||||
return this.options.notebookDirectory;
|
||||
}
|
||||
return path.dirname(this.options.documentPath);
|
||||
}
|
||||
|
||||
private matchUrlAndPort(data: string | Buffer): [vscode.Uri, string] {
|
||||
// regex: Looks for the successful startup log message like:
|
||||
// [C 12:08:51.947 NotebookApp]
|
||||
//
|
||||
// Copy/paste this URL into your browser when you connect for the first time,
|
||||
// to login with a token:
|
||||
// http://localhost:8888/?token=f5ee846e9bd61c3a8d835ecd9b965591511a331417b997b7
|
||||
let dataString = data.toString();
|
||||
let urlMatch = dataString.match(/\[C[\s\S]+ {8}(.+:(\d+)\/.*)$/m);
|
||||
|
||||
if (urlMatch) {
|
||||
// Legacy case: manually parse token info if no token/port were passed
|
||||
return [vscode.Uri.parse(urlMatch[1]), urlMatch[2]];
|
||||
} else if (this._uri && dataString.indexOf(JupyterStartedMessage) > -1) {
|
||||
// Default case: detect the notebook started message, indicating our preferred port and token were used
|
||||
return [this._uri, this._port];
|
||||
}
|
||||
return [undefined, undefined];
|
||||
}
|
||||
|
||||
private notifyStarted(install: JupyterServerInstallation, jupyterUri: string): void {
|
||||
install.outputChannel.appendLine(localize('jupyterOutputMsgStartSuccessful', '... Jupyter is running at {0}', jupyterUri));
|
||||
}
|
||||
private notify(install: JupyterServerInstallation, message: string): void {
|
||||
install.outputChannel.appendLine(message);
|
||||
}
|
||||
|
||||
private notifyStarting(install: JupyterServerInstallation, startCommand: string): void {
|
||||
install.outputChannel.appendLine(localize('jupyterOutputMsgStart', '... Starting Notebook server'));
|
||||
install.outputChannel.appendLine(` > ${startCommand}`);
|
||||
}
|
||||
|
||||
private spawnJupyterProcess(install: JupyterServerInstallation, startCommand: string): ChildProcess {
|
||||
// Specify the global environment variables
|
||||
let env = this.getEnvWithConfigPaths();
|
||||
// Setting the PATH variable here for the jupyter command. Apparently setting it above will cause the
|
||||
// notebook process to die even though we don't override it with the for loop logic above.
|
||||
delete env['Path'];
|
||||
env['PATH'] = install.pythonEnvVarPath;
|
||||
|
||||
// 'MSHOST_TELEMETRY_ENABLED' and 'MSHOST_ENVIRONMENT' environment variables are set
|
||||
// for telemetry purposes used by PROSE in the process where the Jupyter kernel runs
|
||||
if (vscode.workspace.getConfiguration('telemetry').get<boolean>('enableTelemetry', true)) {
|
||||
env['MSHOST_TELEMETRY_ENABLED'] = true;
|
||||
} else {
|
||||
env['MSHOST_TELEMETRY_ENABLED'] = false;
|
||||
}
|
||||
|
||||
env['MSHOST_ENVIRONMENT'] = 'ADSClient-' + vscode.version;
|
||||
|
||||
// Start the notebook process
|
||||
let options = {
|
||||
shell: true,
|
||||
env: env
|
||||
};
|
||||
let childProcess = this.utils.spawn(startCommand, [], options);
|
||||
return childProcess;
|
||||
}
|
||||
|
||||
private getEnvWithConfigPaths(): any {
|
||||
let env = Object.assign({}, process.env);
|
||||
env['JUPYTER_CONFIG_DIR'] = this.instanceConfigRoot;
|
||||
env['JUPYTER_PATH'] = this.instanceDataRoot;
|
||||
return env;
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorHandler {
|
||||
private numErrors: number = 0;
|
||||
|
||||
public handleError(error: Error): ErrorAction {
|
||||
this.numErrors++;
|
||||
return this.numErrors > 3 ? ErrorAction.Shutdown : ErrorAction.Continue;
|
||||
}
|
||||
}
|
||||
|
||||
enum ErrorAction {
|
||||
Continue = 1,
|
||||
Shutdown = 2
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import * as vscode from 'vscode';
|
||||
|
||||
export class MockExtensionContext implements vscode.ExtensionContext {
|
||||
logger: undefined;
|
||||
logDirectory: './';
|
||||
logPath: './';
|
||||
subscriptions: { dispose(): any; }[];
|
||||
workspaceState: vscode.Memento;
|
||||
globalState: vscode.Memento;
|
||||
|
||||
Reference in New Issue
Block a user