/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { flatten, groupBy, isNonEmptyArray } from 'vs/base/common/arrays'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { combinedDisposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ILanguageService } from 'vs/editor/common/services/languageService'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { NotebookDto } from 'vs/workbench/api/browser/mainThreadNotebookDto'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/notebookEditorService'; import { CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { INotebookKernel, INotebookKernelChangeEvent, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import { ExtHostContext, ExtHostNotebookKernelsShape, ICellExecuteUpdateDto, ICellExecutionCompleteDto, IExtHostContext, INotebookKernelDto2, MainContext, MainThreadNotebookKernelsShape } from '../common/extHost.protocol'; abstract class MainThreadKernel implements INotebookKernel { private readonly _onDidChange = new Emitter(); private readonly preloads: { uri: URI, provides: string[]; }[]; readonly onDidChange: Event = this._onDidChange.event; readonly id: string; readonly viewType: string; readonly extension: ExtensionIdentifier; implementsInterrupt: boolean; label: string; description?: string; detail?: string; kind?: string; supportedLanguages: string[]; implementsExecutionOrder: boolean; localResourceRoot: URI; public get preloadUris() { return this.preloads.map(p => p.uri); } public get preloadProvides() { return flatten(this.preloads.map(p => p.provides)); } constructor(data: INotebookKernelDto2, private _languageService: ILanguageService) { this.id = data.id; this.viewType = data.notebookType; this.extension = data.extensionId; this.implementsInterrupt = data.supportsInterrupt ?? false; this.label = data.label; this.description = data.description; this.detail = data.detail; this.kind = data.kind; this.supportedLanguages = isNonEmptyArray(data.supportedLanguages) ? data.supportedLanguages : _languageService.getRegisteredLanguageIds(); this.implementsExecutionOrder = data.supportsExecutionOrder ?? false; this.localResourceRoot = URI.revive(data.extensionLocation); this.preloads = data.preloads?.map(u => ({ uri: URI.revive(u.uri), provides: u.provides })) ?? []; } update(data: Partial) { const event: INotebookKernelChangeEvent = Object.create(null); if (data.label !== undefined) { this.label = data.label; event.label = true; } if (data.description !== undefined) { this.description = data.description; event.description = true; } if (data.detail !== undefined) { this.detail = data.detail; event.detail = true; } if (data.kind !== undefined) { this.kind = data.kind; event.kind = true; } if (data.supportedLanguages !== undefined) { this.supportedLanguages = isNonEmptyArray(data.supportedLanguages) ? data.supportedLanguages : this._languageService.getRegisteredLanguageIds(); event.supportedLanguages = true; } if (data.supportsExecutionOrder !== undefined) { this.implementsExecutionOrder = data.supportsExecutionOrder; event.hasExecutionOrder = true; } this._onDidChange.fire(event); } abstract executeNotebookCellsRequest(uri: URI, cellHandles: number[]): Promise; abstract cancelNotebookCellExecution(uri: URI, cellHandles: number[]): Promise; } @extHostNamedCustomer(MainContext.MainThreadNotebookKernels) export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape { private readonly _editors = new Map(); private readonly _disposables = new DisposableStore(); private readonly _kernels = new Map(); private readonly _proxy: ExtHostNotebookKernelsShape; private readonly _executions = new Set(); constructor( extHostContext: IExtHostContext, @ILanguageService private readonly _languageService: ILanguageService, @INotebookKernelService private readonly _notebookKernelService: INotebookKernelService, @INotebookExecutionStateService private readonly _notebookExecutionStateService: INotebookExecutionStateService, @INotebookEditorService notebookEditorService: INotebookEditorService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebookKernels); notebookEditorService.listNotebookEditors().forEach(this._onEditorAdd, this); notebookEditorService.onDidAddNotebookEditor(this._onEditorAdd, this, this._disposables); notebookEditorService.onDidRemoveNotebookEditor(this._onEditorRemove, this, this._disposables); this._disposables.add(toDisposable(() => { // EH shut down, complete all executions started by this EH this._executions.forEach(e => { const uri = CellUri.parse(URI.parse(e)); if (uri) { this._notebookExecutionStateService.completeNotebookCellExecution(uri.notebook, uri.handle, { }); } }); })); } dispose(): void { this._disposables.dispose(); for (let [, registration] of this._kernels.values()) { registration.dispose(); } } // --- kernel ipc private _onEditorAdd(editor: INotebookEditor) { const ipcListener = editor.onDidReceiveMessage(e => { if (!editor.hasModel()) { return; } const { selected } = this._notebookKernelService.getMatchingKernel(editor.textModel); if (!selected) { return; } for (let [handle, candidate] of this._kernels) { if (candidate[0] === selected) { this._proxy.$acceptKernelMessageFromRenderer(handle, editor.getId(), e.message); break; } } }); this._editors.set(editor, ipcListener); } private _onEditorRemove(editor: INotebookEditor) { this._editors.get(editor)?.dispose(); this._editors.delete(editor); } async $postMessage(handle: number, editorId: string | undefined, message: any): Promise { const tuple = this._kernels.get(handle); if (!tuple) { throw new Error('kernel already disposed'); } const [kernel] = tuple; let didSend = false; for (const [editor] of this._editors) { if (!editor.hasModel()) { continue; } if (this._notebookKernelService.getMatchingKernel(editor.textModel).selected !== kernel) { // different kernel continue; } if (editorId === undefined) { // all editors editor.postMessage(message); didSend = true; } else if (editor.getId() === editorId) { // selected editors editor.postMessage(message); didSend = true; break; } } return didSend; } // --- kernel adding/updating/removal async $addKernel(handle: number, data: INotebookKernelDto2): Promise { const that = this; const kernel = new class extends MainThreadKernel { async executeNotebookCellsRequest(uri: URI, handles: number[]): Promise { await that._proxy.$executeCells(handle, uri, handles); } async cancelNotebookCellExecution(uri: URI, handles: number[]): Promise { await that._proxy.$cancelCells(handle, uri, handles); } }(data, this._languageService); const listener = this._notebookKernelService.onDidChangeSelectedNotebooks(e => { if (e.oldKernel === kernel.id) { this._proxy.$acceptNotebookAssociation(handle, e.notebook, false); } else if (e.newKernel === kernel.id) { this._proxy.$acceptNotebookAssociation(handle, e.notebook, true); } }); const registration = this._notebookKernelService.registerKernel(kernel); this._kernels.set(handle, [kernel, combinedDisposable(listener, registration)]); } $updateKernel(handle: number, data: Partial): void { const tuple = this._kernels.get(handle); if (tuple) { tuple[0].update(data); } } $removeKernel(handle: number): void { const tuple = this._kernels.get(handle); if (tuple) { tuple[1].dispose(); this._kernels.delete(handle); } } $updateNotebookPriority(handle: number, notebook: UriComponents, value: number | undefined): void { const tuple = this._kernels.get(handle); if (tuple) { this._notebookKernelService.updateKernelNotebookAffinity(tuple[0], URI.revive(notebook), value); } } // --- execution $addExecution(rawUri: UriComponents, cellHandle: number): void { const uri = URI.revive(rawUri); this._notebookExecutionStateService.createNotebookCellExecution(uri, cellHandle); const cellUri = CellUri.generateCellUri(uri, cellHandle, uri.scheme); this._executions.add(cellUri.toString()); } $updateExecutions(data: SerializableObjectWithBuffers): void { const updates = data.value; const groupedUpdates = groupBy(updates, (a, b) => a.cellHandle - b.cellHandle); groupedUpdates.forEach(datas => { const first = datas[0]; try { const uri = URI.revive(first.uri); this._notebookExecutionStateService.updateNotebookCellExecution(uri, first.cellHandle, datas.map(NotebookDto.fromCellExecuteUpdateDto)); } catch (e) { onUnexpectedError(e); } }); } $completeExecution(rawUri: UriComponents, cellHandle: number, data: SerializableObjectWithBuffers): void { const uri = URI.revive(rawUri); this._notebookExecutionStateService.completeNotebookCellExecution(uri, cellHandle, NotebookDto.fromCellExecuteCompleteDto(data.value)); const cellUri = CellUri.generateCellUri(uri, cellHandle, uri.scheme); this._executions.delete(cellUri.toString()); } }