mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-20 17:22:51 -05:00
238 lines
9.2 KiB
TypeScript
238 lines
9.2 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import * as azdata from 'azdata';
|
|
|
|
import * as path from 'path';
|
|
import * as vscode from 'vscode';
|
|
import * as nls from 'vscode-nls';
|
|
import { getDateTimeString, getErrorMessage } from '../common/utils';
|
|
import { NotebookPathInfo } from '../interfaces';
|
|
import { IPlatformService } from './platformService';
|
|
import * as loc from '../localizedConstants';
|
|
const localize = nls.loadMessageBundle();
|
|
|
|
export interface Notebook {
|
|
cells: NotebookCell[];
|
|
}
|
|
|
|
export interface NotebookCell {
|
|
cell_type: 'code';
|
|
source: string[];
|
|
metadata: {};
|
|
outputs: string[];
|
|
execution_count: number;
|
|
}
|
|
|
|
export interface NotebookExecutionResult {
|
|
succeeded: boolean;
|
|
outputNotebook?: string;
|
|
errorMessage?: string;
|
|
}
|
|
|
|
export interface INotebookService {
|
|
openNotebook(notebook: string | NotebookPathInfo): Promise<azdata.nb.NotebookEditor>;
|
|
openNotebookWithEdits(notebook: string | NotebookPathInfo, cellStatements: string[], insertionPosition?: number): Promise<azdata.nb.NotebookEditor>;
|
|
openNotebookWithContent(title: string, content: string): Promise<azdata.nb.NotebookEditor>;
|
|
getNotebook(notebook: string | NotebookPathInfo): Promise<Notebook>;
|
|
getNotebookPath(notebook: string | NotebookPathInfo): string;
|
|
executeNotebook(notebook: any, env?: NodeJS.ProcessEnv): Promise<NotebookExecutionResult>;
|
|
backgroundExecuteNotebook(taskName: string | undefined, notebookInfo: string | NotebookPathInfo | Notebook, tempNotebookPrefix: string, platformService: IPlatformService, env?: NodeJS.ProcessEnv): void;
|
|
}
|
|
|
|
|
|
export class NotebookService implements INotebookService {
|
|
|
|
constructor(private platformService: IPlatformService, private extensionPath: string) { }
|
|
|
|
/**
|
|
* Open notebook with file path
|
|
* @param notebook the path of the notebook
|
|
*/
|
|
async openNotebook(notebook: string | NotebookPathInfo): Promise<azdata.nb.NotebookEditor> {
|
|
const notebookPath = await this.getNotebookFullPath(notebook);
|
|
return await this.showNotebookAsUntitled(notebookPath);
|
|
}
|
|
|
|
/**
|
|
* Inserts cell code given by {@param cellStatements} in an existing notebook given by {@param notebook} file path at the location
|
|
* {@param insertionPosition} and then opens the edited notebook.
|
|
*
|
|
* @param notebook - the path to notebook that needs to be opened
|
|
* @param cellStatements - array of statements to be inserted in a cell
|
|
* @param insertionPosition - the position at which cells are inserted. Default is a new cell at the beginning of the notebook.
|
|
*/
|
|
async openNotebookWithEdits(notebook: string, cellStatements: string[], insertionPosition: number = 0): Promise<azdata.nb.NotebookEditor> {
|
|
const openedNotebook = await this.openNotebook(notebook);
|
|
await openedNotebook.edit((editBuilder: azdata.nb.NotebookEditorEdit) => {
|
|
editBuilder.insertCell({
|
|
cell_type: 'code',
|
|
source: cellStatements
|
|
}, insertionPosition);
|
|
});
|
|
return openedNotebook;
|
|
}
|
|
|
|
/**
|
|
* Open notebook with given contents
|
|
* @param title the title of the notebook
|
|
* @param content the notebook content
|
|
*/
|
|
async openNotebookWithContent(title: string, content: string): Promise<azdata.nb.NotebookEditor> {
|
|
const uri: vscode.Uri = vscode.Uri.parse(`untitled:${this.findNextUntitledEditorName(title)}`);
|
|
return await azdata.nb.showNotebookDocument(uri, {
|
|
connectionProfile: undefined,
|
|
preview: false,
|
|
initialContent: content,
|
|
initialDirtyState: false
|
|
});
|
|
}
|
|
|
|
|
|
async getNotebook(notebook: string | NotebookPathInfo): Promise<Notebook> {
|
|
const notebookPath = await this.getNotebookFullPath(notebook);
|
|
return <Notebook>JSON.parse(await this.platformService.readTextFile(notebookPath));
|
|
}
|
|
|
|
async executeNotebook(notebook: Notebook, env?: NodeJS.ProcessEnv): Promise<NotebookExecutionResult> {
|
|
const content = JSON.stringify(notebook, undefined, 4);
|
|
const fileName = `nb-${getDateTimeString()}.ipynb`;
|
|
const workingDirectory = this.platformService.storagePath();
|
|
const notebookFullPath = path.join(workingDirectory, fileName);
|
|
const outputFullPath = path.join(workingDirectory, `output-${fileName}`);
|
|
const additionalEnvironmentVariables: NodeJS.ProcessEnv = env || {};
|
|
// Set the azdata eula
|
|
// Scenarios using the executeNotebook feature already have the EULA accepted by the user before executing this.
|
|
additionalEnvironmentVariables['ACCEPT_EULA'] = 'yes';
|
|
try {
|
|
await this.platformService.saveTextFile(content, notebookFullPath);
|
|
await this.platformService.runCommand(`azdata notebook run --path "${notebookFullPath}" --output-path "${workingDirectory}" --timeout -1`,
|
|
{
|
|
additionalEnvironmentVariables: additionalEnvironmentVariables,
|
|
workingDirectory: workingDirectory
|
|
});
|
|
return {
|
|
succeeded: true
|
|
};
|
|
}
|
|
catch (error) {
|
|
const outputExists = await this.platformService.fileExists(outputFullPath);
|
|
return {
|
|
succeeded: false,
|
|
outputNotebook: outputExists ? await this.platformService.readTextFile(outputFullPath) : undefined,
|
|
errorMessage: getErrorMessage(error)
|
|
};
|
|
} finally {
|
|
this.platformService.deleteFile(notebookFullPath);
|
|
this.platformService.deleteFile(outputFullPath);
|
|
}
|
|
}
|
|
|
|
backgroundExecuteNotebook(taskName: string = 'Executing notebook', notebookInfo: string | NotebookPathInfo | Notebook, tempNotebookPrefix: string, platformService: IPlatformService, env?: NodeJS.ProcessEnv): void {
|
|
azdata.tasks.startBackgroundOperation({
|
|
displayName: taskName,
|
|
description: taskName,
|
|
isCancelable: false,
|
|
operation: async op => {
|
|
op.updateStatus(azdata.TaskStatus.InProgress);
|
|
const notebook = (typeof notebookInfo === 'object' && 'cells' in notebookInfo)
|
|
? <Notebook>notebookInfo
|
|
: await this.getNotebook(notebookInfo);
|
|
const result = await this.executeNotebook(notebook, env);
|
|
if (result.succeeded) {
|
|
op.updateStatus(azdata.TaskStatus.Succeeded);
|
|
} else {
|
|
op.updateStatus(azdata.TaskStatus.Failed, result.errorMessage);
|
|
if (result.outputNotebook) {
|
|
const taskFailedMessage = loc.backgroundExecutionFailed(taskName);
|
|
const selectedOption = await vscode.window.showErrorMessage(taskFailedMessage, loc.viewErrorDetail);
|
|
platformService.logToOutputChannel(taskFailedMessage);
|
|
if (selectedOption === loc.viewErrorDetail) {
|
|
try {
|
|
await this.openNotebookWithContent(`${tempNotebookPrefix}-${getDateTimeString()}`, result.outputNotebook);
|
|
} catch (error) {
|
|
const openNotebookError = loc.failedToOpenNotebook(error);
|
|
platformService.logToOutputChannel(openNotebookError);
|
|
vscode.window.showErrorMessage(openNotebookError);
|
|
}
|
|
}
|
|
} else {
|
|
const errorMessage = loc.taskFailedWithNoOutputNotebook(taskName);
|
|
platformService.logToOutputChannel(errorMessage);
|
|
vscode.window.showErrorMessage(errorMessage);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
async getNotebookFullPath(notebook: string | NotebookPathInfo): Promise<string> {
|
|
const notebookPath = this.getNotebookPath(notebook);
|
|
let notebookExists = await this.platformService.fileExists(notebookPath);
|
|
if (notebookExists) {
|
|
// this is for the scenarios when the provider is in a different extension, the full path will be passed in.
|
|
return notebookPath;
|
|
}
|
|
|
|
// this is for the scenarios in this extension, the notebook paths are relative path.
|
|
const absolutePath = path.join(this.extensionPath, notebookPath);
|
|
notebookExists = await this.platformService.fileExists(absolutePath);
|
|
if (notebookExists) {
|
|
return absolutePath;
|
|
} else {
|
|
throw new Error(localize('resourceDeployment.notebookNotFound', "The notebook {0} does not exist", notebookPath));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* get the notebook path for current platform
|
|
* @param notebook the notebook path
|
|
*/
|
|
getNotebookPath(notebook: string | NotebookPathInfo): string {
|
|
let notebookPath;
|
|
if (notebook && (typeof notebook !== 'string')) {
|
|
const platform = this.platformService.platform();
|
|
if (platform === 'win32') {
|
|
notebookPath = notebook.win32;
|
|
} else if (platform === 'darwin') {
|
|
notebookPath = notebook.darwin;
|
|
} else {
|
|
notebookPath = notebook.linux;
|
|
}
|
|
} else {
|
|
notebookPath = notebook;
|
|
}
|
|
return notebookPath;
|
|
}
|
|
|
|
findNextUntitledEditorName(filePath: string): string {
|
|
const fileExtension = path.extname(filePath);
|
|
const baseName = path.basename(filePath, fileExtension);
|
|
let idx = 0;
|
|
let title = `${baseName}`;
|
|
do {
|
|
const suffix = idx === 0 ? '' : `-${idx}`;
|
|
title = `${baseName}${suffix}`;
|
|
idx++;
|
|
} while (this.platformService.isNotebookNameUsed(title));
|
|
|
|
return title;
|
|
}
|
|
|
|
async showNotebookAsUntitled(notebookPath: string): Promise<azdata.nb.NotebookEditor> {
|
|
let targetFileName: string = this.findNextUntitledEditorName(notebookPath);
|
|
const untitledFileName: vscode.Uri = vscode.Uri.parse(`untitled:${targetFileName}`);
|
|
const document = await vscode.workspace.openTextDocument(notebookPath);
|
|
let initialContent = document.getText();
|
|
return await azdata.nb.showNotebookDocument(untitledFileName, {
|
|
connectionProfile: undefined,
|
|
preview: false,
|
|
initialContent: initialContent,
|
|
initialDirtyState: false
|
|
});
|
|
}
|
|
}
|
|
|