diff --git a/src/api.proposed.kernelApi.d.ts b/src/api.proposed.kernelApi.d.ts index 62f9f4a21d1..244edcc9d0b 100644 --- a/src/api.proposed.kernelApi.d.ts +++ b/src/api.proposed.kernelApi.d.ts @@ -11,6 +11,11 @@ declare module './api' { >; } export interface Kernels { + /** + * Whether the access to the Kernels has been revoked. + * This happens when the user has not provided consent to the API being used by the requesting extension. + */ + isRevoked: boolean; /** * Finds a kernel for a given resource. * For instance if the resource is a notebook, then look for a kernel associated with the given Notebook document. diff --git a/src/api.proposed.kernelApiAccess.d.ts b/src/api.proposed.kernelApiAccess.d.ts index b48d2e5104e..fc02f9b498b 100644 --- a/src/api.proposed.kernelApiAccess.d.ts +++ b/src/api.proposed.kernelApiAccess.d.ts @@ -5,12 +5,11 @@ import type { CancellationToken } from 'vscode'; declare module './api' { - /** - * Provides access to the Jupyter Kernel API. - * As Kernels can be used to execute code on local or remote machines, this poses a threat to security. - * As a result users will be prompted to allow access to the Kernel API. - */ export interface Jupyter { - getKernelApi(): Promise; + /** + * Request access to Kernels. + * As Kernels can be used to execute code on local or remote machines, user consent will be required. + */ + requestKernelAccess(): Thenable; } } diff --git a/src/api.proposed.kernelCodeExecution.d.ts b/src/api.proposed.kernelCodeExecution.d.ts index 024aa8563d9..04ca5a4f2a7 100644 --- a/src/api.proposed.kernelCodeExecution.d.ts +++ b/src/api.proposed.kernelCodeExecution.d.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import type { CancellationToken, Event } from 'vscode'; +import type { CancellationToken } from 'vscode'; declare module './api' { interface OutputItem { @@ -14,28 +14,16 @@ declare module './api' { */ data: Uint8Array; } - - interface ExecutionResult { - /** - * Resolves when the execution has completed. - */ - done: Promise; - - /** - * Event fired with the output items emitted as a result of the execution. - */ - onDidEmitOutput: Event; - } export interface Kernel { /** * Executes code in the kernel. * The code executed will not result in changes to the execution count * & will not show up in the Kernel execution history. * - * @param {string} code Code to be executed. - * @param {CancellationToken} token Triggers the cancellation of the execution. - * @return {*} {ExecutionResult} + * @param code Code to be executed. + * @param token Triggers the cancellation of the execution. + * @returns Async iterable of output items, that completes when the execution is complete. */ - executeCode(code: string, token: CancellationToken): ExecutionResult; + executeCode(code: string, token: CancellationToken): AsyncIterable; } } diff --git a/src/extension.node.ts b/src/extension.node.ts index 69476c234e2..f7f1073d4bc 100644 --- a/src/extension.node.ts +++ b/src/extension.node.ts @@ -140,7 +140,7 @@ export async function activate(context: IExtensionContext): Promise { throw new Error('Not Implemented'); }, - getKernelApi: () => Promise.resolve(undefined) + requestKernelAccess: () => Promise.reject(new Error('Not Implemented')) }; } } diff --git a/src/extension.web.ts b/src/extension.web.ts index 4092d0c7ed0..2c9ac8e5c74 100644 --- a/src/extension.web.ts +++ b/src/extension.web.ts @@ -144,7 +144,7 @@ export async function activate(context: IExtensionContext): Promise { throw new Error('Not Implemented'); }, - getKernelApi: () => Promise.resolve(undefined) + requestKernelAccess: () => Promise.reject(new Error('Not Implemented')) }; } } diff --git a/src/kernels/api/api.ts b/src/kernels/api/api.ts index 385eed46cb6..72ea4e7570d 100644 --- a/src/kernels/api/api.ts +++ b/src/kernels/api/api.ts @@ -20,6 +20,7 @@ export function getKernelsApi(extensionId: string): Kernels { } api = { + isRevoked: false, findKernel(query: { uri: Uri }) { const kernelProvider = ServiceContainer.instance.get(IKernelProvider); const notebooks = ServiceContainer.instance.get(IVSCodeNotebook); diff --git a/src/kernels/api/api.vscode.common.test.ts b/src/kernels/api/api.vscode.common.test.ts index 1b9716f3fe1..c9391428d7d 100644 --- a/src/kernels/api/api.vscode.common.test.ts +++ b/src/kernels/api/api.vscode.common.test.ts @@ -24,10 +24,10 @@ import { } from '../../test/datascience/notebook/helper'; import { getKernelsApi } from './api'; import { raceTimeoutError } from '../../platform/common/utils/async'; -import { ExecutionResult } from '../../api'; import { dispose } from '../../platform/common/utils/lifecycle'; import { IKernel, IKernelProvider } from '../types'; import { IControllerRegistration, IVSCodeNotebookController } from '../../notebooks/controllers/types'; +import { OutputItem } from '../../api'; suiteMandatory('Kernel API Tests @python', function () { const disposables: IDisposable[] = []; @@ -127,29 +127,28 @@ suiteMandatory('Kernel API Tests @python', function () { ); }); - async function waitForOutput(executionResult: ExecutionResult, expectedOutput: string, expectedMimetype: string) { + async function waitForOutput( + executionResult: AsyncIterable, + expectedOutput: string, + expectedMimetype: string + ) { const disposables: IDisposable[] = []; const outputsReceived: string[] = []; - const outputPromise = new Promise((resolve, reject) => { - executionResult.onDidEmitOutput( - (e) => { - traceInfo(`Output received ${e.length} & mime types are ${e.map((item) => item.mime).join(', ')}}`); - e.forEach((item) => { - if (item.mime === expectedMimetype) { - const output = new TextDecoder().decode(item.data).trim(); - if (output === expectedOutput.trim()) { - resolve(); - } else { - reject(new Error(`Unexpected output ${output}`)); - } + const outputPromise = new Promise(async (resolve, reject) => { + for await (const items of executionResult) { + items.forEach((item) => { + if (item.mime === expectedMimetype) { + const output = new TextDecoder().decode(item.data).trim(); + if (output === expectedOutput.trim()) { + resolve(); } else { - outputsReceived.push(`${item.mime} ${new TextDecoder().decode(item.data).trim()}`); + reject(new Error(`Unexpected output ${output}`)); } - }); - }, - undefined, - disposables - ); + } else { + outputsReceived.push(`${item.mime} ${new TextDecoder().decode(item.data).trim()}`); + } + }); + } }); await raceTimeoutError( diff --git a/src/kernels/api/kernel.ts b/src/kernels/api/kernel.ts index b2b40e7d0a4..8e34a480f3c 100644 --- a/src/kernels/api/kernel.ts +++ b/src/kernels/api/kernel.ts @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { l10n, CancellationToken, Event, EventEmitter, ProgressLocation, extensions, window, Disposable } from 'vscode'; -import { ExecutionResult, Kernel } from '../../api'; +import { l10n, CancellationToken, ProgressLocation, extensions, window, Disposable, Event } from 'vscode'; +import { Kernel, OutputItem } from '../../api'; import { ServiceContainer } from '../../platform/ioc/container'; import { IKernel, IKernelProvider, INotebookKernelExecution } from '../types'; import { getDisplayNameOrNameOfKernelConnection } from '../helpers'; @@ -123,7 +123,7 @@ class WrappedKernelPerExtension implements Kernel { once(kernel.onDisposed)(() => this.progress.dispose()); } - executeCode(code: string, token: CancellationToken): ExecutionResult { + async *executeCode(code: string, token: CancellationToken): AsyncGenerator { this.previousProgress?.dispose(); let completed = false; const measures = { @@ -156,7 +156,7 @@ class WrappedKernelPerExtension implements Kernel { if (!this.kernel.session?.kernel) { properties.failed = true; sendApiExecTelemetry(this.kernel, measures, properties).catch(noop); - if (this.status === 'dead' || this.status === 'terminating') { + if (this.kernel.status === 'dead' || this.kernel.status === 'terminating') { throw new Error('Kernel is dead or terminating'); } throw new Error('Kernel connection not available to execute 3rd party code'); @@ -164,8 +164,6 @@ class WrappedKernelPerExtension implements Kernel { const disposables: IDisposable[] = []; const done = createDeferred(); - const onDidEmitOutput = new EventEmitter<{ mime: string; data: Uint8Array }[]>(); - disposables.push(onDidEmitOutput); disposables.push({ dispose: () => { measures.duration = stopwatch.elapsedTime; @@ -179,6 +177,8 @@ class WrappedKernelPerExtension implements Kernel { const kernelExecution = ServiceContainer.instance .get(IKernelProvider) .getKernelExecution(this.kernel); + const outputs: OutputItem[][] = []; + let outputsReceieved = createDeferred(); kernelExecution .executeCode(code, this.extensionId, token) .then((codeExecution) => { @@ -206,7 +206,7 @@ class WrappedKernelPerExtension implements Kernel { codeExecution.onDidEmitOutput( (e) => { e.forEach((item) => mimeTypes.add(item.mime)); - onDidEmitOutput.fire(e); + outputs.push(e); }, this, disposables @@ -232,10 +232,18 @@ class WrappedKernelPerExtension implements Kernel { this, disposables ); - return { - done: done.promise, - onDidEmitOutput: onDidEmitOutput.event - }; + while (true) { + await Promise.race([outputsReceieved.promise, done.promise]); + if (outputsReceieved.completed) { + outputsReceieved = createDeferred(); + } + while (outputs.length) { + yield outputs.shift()!; + } + if (done.completed) { + break; + } + } } } diff --git a/src/standalone/api/api.ts b/src/standalone/api/api.ts index 51dff526e69..ab7ef8ce5c4 100644 --- a/src/standalone/api/api.ts +++ b/src/standalone/api/api.ts @@ -213,8 +213,8 @@ export function buildApi( extensions.determineExtensionFromCallStack().extensionId ); }, - getKernelApi() { - return Promise.resolve(undefined); + requestKernelAccess() { + return Promise.reject(new Error('Not Implemeted')); } };