Skip to content

Commit 35bc7e2

Browse files
Add API method to shutdown Jupyter kernels programmatically (Proposed API) (#16775)
* AI ready repo * Updates * Updates * Initial plan * Implement shutdown kernel command infrastructure Co-authored-by: DonJayamanne <[email protected]> * Add shutdown kernel command to VS Code UI and add unit test Co-authored-by: DonJayamanne <[email protected]> * Replace string URI comparison with isEqual function in shutdownKernel Co-authored-by: DonJayamanne <[email protected]> * Address feedback: Remove kernelConnector changes, call kernel.shutdown() and dispose() directly, add integration test Co-authored-by: DonJayamanne <[email protected]> * Fix integration test to properly call both shutdown() and dispose() methods Co-authored-by: DonJayamanne <[email protected]> * Remove 'shutdown' from KernelAction type as requested Co-authored-by: DonJayamanne <[email protected]> * Address feedback: Remove kernelConnector test, separate try-catch for dispose, support multiple argument types, move test to new file Co-authored-by: DonJayamanne <[email protected]> * Updates * Fix formatting * Convert shutdown command to API-based approach Co-authored-by: DonJayamanne <[email protected]> * Updated instructions * Fix test assertion and address linting/formatting issues - Replace assert.isNotEmpty() with assert.isDefined() in shutdown test - Fix incorrect assertion that was causing test failure when apiKernel is undefined - Run format-fix to ensure consistent code formatting Co-authored-by: DonJayamanne <[email protected]> * Move shutdown test to api.vscode.test.ts and delete separate test file - Moved "Can shutdown a kernel correctly via API" test from shutdown.vscode.test.ts to api.vscode.test.ts - Added necessary imports for kernels API, test helpers, and execution functions - Updated test to create separate notebook to avoid conflicts with existing tests - Deleted standalone shutdown.vscode.test.ts file as requested - Fixed TypeScript compilation by removing unused IKernel import Co-authored-by: DonJayamanne <[email protected]> * Create api.proposed.shutdown.d.ts with authorization check Co-authored-by: DonJayamanne <[email protected]> * Fix formatting * Fix API test by setting disableUI: false for user-started kernel Co-authored-by: DonJayamanne <[email protected]> * Fix tests * Updates to allow list --------- Co-authored-by: Don Jayamanne <[email protected]> Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: DonJayamanne <[email protected]>
1 parent b8bf64e commit 35bc7e2

File tree

8 files changed

+156
-17
lines changed

8 files changed

+156
-17
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
description: Plan to fix a bug
3+
tools: ['codebase', 'editFiles', 'fetch', 'findTestFiles', 'problems', 'runTasks', 'runTests', 'search', 'terminalLastCommand', 'testFailure', 'usages', 'vscodeAPI', 'github', 'get_issue', 'get_issue_comments', 'get_me', 'copilotCodingAgent']
4+
---
5+
# Bug fixing mode instructions
6+
You are an expert (TypeScript and Python) software engineer tasked with fixing a bug in the codebase.
7+
Your goal is to prepare a detailed plan to fix the bug, for this you first need to:
8+
* Understand the context of the bug by reading the issue description and comments.
9+
* Understand the codebase by reading the relevant instruction files.
10+
* Identify the root cause of the bug, and explain this to the user.
11+
12+
Based on your above understanding generate a plan to fix the bug.
13+
Ensure the plan consists of a Markdown document that has the following sections:
14+
15+
* Overview: A brief description of the bug.
16+
* Root Cause: A detailed explanation of the root cause of the bug, including any relevant code snippets or references to the codebase.
17+
* Requirements: A list of requirements to resolve the bug.
18+
* Implementation Steps: A detailed list of steps to implement the bug fix.
19+
20+
Finally prompt the user if they would like to proceed with the implementation of the bug fix.
21+
Remember, do not make any code edits, just generate a plan.
22+
When implementing the fix, ensure to add unit tests to verify the fix.

.github/copilot-instructions.md

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,9 @@ src/ # Source code for the extension
8787
└── test/ # Integration, unit and end-to-end tests
8888
```
8989

90-
### Component Instructions
90+
### Component-Specific Instructions
9191

92-
**IMPORTANT**: Before modifying code in directories below, you **MUST** read the corresponding instruction file(s).
92+
**IMPORTANT**: Before modifying any code in the directories listed below, you **MUST** first read the corresponding instruction file to understand the specific conventions, patterns, and architectural requirements for that component.
9393

9494
Each component has detailed guidelines that cover:
9595
- Architecture patterns and design principles
@@ -99,17 +99,18 @@ Each component has detailed guidelines that cover:
9999
- Error handling approaches
100100
- Component-specific best practices
101101

102-
103-
| Directory | Instruction File | Purpose |
104-
|-----------|------------------|---------|
105-
| `src/platform/**` | `.github/instructions/platform.instructions.md` | Cross-platform abstractions |
106-
| `src/kernels/**` | `.github/instructions/kernel.instructions.md` | Kernel management |
107-
| `src/kernels/jupyter/**` | `.github/instructions/kernel-jupyter.instructions.md` | Jupyter protocol |
108-
| `src/notebooks/**` | `.github/instructions/notebooks.instructions.md` | Notebook controllers |
109-
| `src/interactive-window/**` | `.github/instructions/interactiveWindow.instructions.md` | REPL functionality |
110-
| `src/standalone/**` | `.github/instructions/standalone.instructions.md` | Standalone features |
111-
| `src/notebooks/controllers/ipywidgets/**` | `.github/instructions/ipywidgets.instructions.md` | IPython widgets (interactive Notebook outputs) |
112-
| `src/webviews/extension-side/ipywidgets/**` | `.github/instructions/ipywidgets.instructions.md` | IPython Widget (interactive Notebook outputs) communication |
113-
| `src/webviews/webview-side/ipywidgets/**` | `.github/instructions/ipywidgets.instructions.md` | IPython Widget (interactive Notebook outputs) rendering |
114-
115-
**For AI/Copilot**: Always read the relevant instruction file(s) before modifying code in these directories to ensure adherence to component-specific patterns.
102+
#### Required Reading by Directory
103+
104+
| Directory/Component | Instruction File | When to Read |
105+
|-------------------|------------------|--------------|
106+
| `src/platform/**` | `.github/instructions/platform.instructions.md` | Before working with cross-platform abstractions, utilities, or core services |
107+
| `src/kernels/**` | `.github/instructions/kernel.instructions.md` | Before modifying kernel management, execution, or communication logic |
108+
| `src/kernels/jupyter/**` | `.github/instructions/kernel-jupyter.instructions.md` | Before working with Jupyter protocol implementation or Jupyter-specific features |
109+
| `src/notebooks/**` | `.github/instructions/notebooks.instructions.md` | Before modifying notebook controllers, editing, or management features |
110+
| `src/interactive-window/**` | `.github/instructions/interactiveWindow.instructions.md` | Before working with Python Interactive window (REPL) functionality |
111+
| `src/standalone/**` | `.github/instructions/standalone.instructions.md` | Before modifying standalone mode or isolated execution features |
112+
| `src/notebooks/controllers/ipywidgets/**` | `.github/instructions/ipywidgets.instructions.md` | Before working with IPython widget support |
113+
| `src/webviews/extension-side/ipywidgets/**` | `.github/instructions/ipywidgets.instructions.md` | Before modifying extension-side widget communication |
114+
| `src/webviews/webview-side/ipywidgets/**` | `.github/instructions/ipywidgets.instructions.md` | Before working with frontend widget rendering |
115+
116+
**For AI/Copilot**: Always use the `read_file` tool to read the relevant instruction file(s) before analyzing, modifying, or creating code in these directories. This ensures adherence to component-specific patterns and prevents architectural violations.

.github/instructions/typescript.instructions.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,9 @@ mockInstance.then = undefined; // Ensure 'then' is undefined to prevent hanging
3535
- Use `npm run lint` to check for linter issues
3636
- Use `npm run format` to check code style
3737
- Use `npm run format-fix` to auto-fix formatting issues
38+
39+
## PreCommit
40+
Always run the following scripts before committing changes:
41+
- `npm run format-fix` to ensure proper code formatting
42+
- `npm run lint` to check for linter issues and attempt to fix before committing.
43+
- `npm run test:unittests` to check for tests failures and attempt to fix before committing.

src/api.proposed.shutdown.d.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
// Importing a type to ensure this is treated as a module
5+
import type {} from 'vscode';
6+
7+
declare module './api' {
8+
/**
9+
* Represents a Jupyter Kernel.
10+
*/
11+
export interface Kernel {
12+
/**
13+
* Shuts down the kernel and all its associated resources.
14+
* This operation will terminate the kernel process and cannot be undone.
15+
* After shutdown, the kernel becomes unusable and should be disposed of.
16+
*/
17+
shutdown(): Promise<void>;
18+
}
19+
}

src/kernels/kernel.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,41 @@ abstract class BaseKernel implements IBaseKernel {
449449
Promise.all(Array.from(this.hooks.get('restartCompleted') || new Set<Hook>()).map((h) => h())).catch(noop);
450450
}
451451
}
452+
453+
public async shutdown(): Promise<void> {
454+
try {
455+
logger.info(`Shutdown requested ${getDisplayPath(this.uri)}`);
456+
457+
// Cancel any pending starts
458+
this.startCancellation.cancel();
459+
this.startCancellation.dispose();
460+
461+
// Get the current session if it exists
462+
const session = this._jupyterSessionPromise
463+
? await this._jupyterSessionPromise.catch(() => undefined)
464+
: undefined;
465+
466+
if (session) {
467+
logger.info('Shutting down kernel session');
468+
try {
469+
await session.shutdown();
470+
} catch (ex) {
471+
logger.warn(`Failed to shutdown kernel session ${getDisplayPath(this.uri)}`, ex);
472+
// Continue with disposal even if shutdown fails
473+
}
474+
}
475+
476+
// Clean up the session references
477+
this._session = undefined;
478+
this._jupyterSessionPromise = undefined;
479+
this._postInitializedOnStartPromise = undefined;
480+
481+
logger.info(`Shutdown completed ${getDisplayPath(this.uri)}`);
482+
} catch (ex) {
483+
logger.error(`Failed to shutdown kernel ${getDisplayPath(this.uri)}`, ex);
484+
throw ex;
485+
}
486+
}
452487
protected async startJupyterSession(options: IDisplayOptions = new DisplayOptions(false)): Promise<IKernelSession> {
453488
this._startedAtLeastOnce = true;
454489
if (!options.disableUI) {

src/kernels/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@ export interface IBaseKernel extends IAsyncDisposable {
407407
start(options?: IStartOptions): Promise<IKernelSession>;
408408
interrupt(): Promise<void>;
409409
restart(): Promise<void>;
410+
shutdown(): Promise<void>;
410411
addHook(
411412
event: 'didStart',
412413
hook: (session: IKernelSession | undefined, token: CancellationToken) => Promise<void>,

src/standalone/api/kernels/kernel.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,17 @@ class WrappedKernelPerExtension extends DisposableBase implements Kernel {
131131

132132
return that.onDidReceiveDisplayUpdate.bind(this);
133133
},
134-
executeCode: (code: string, token: CancellationToken) => this.executeCode(code, token)
134+
executeCode: (code: string, token: CancellationToken) => this.executeCode(code, token),
135+
get shutdown() {
136+
if (
137+
![JVSC_EXTENSION_ID, POWER_TOYS_EXTENSION_ID].includes(extensionId) &&
138+
!PROPOSED_API_ALLOWED_PUBLISHERS.includes(extensionId.split('.')[0])
139+
) {
140+
throw new Error(`Proposed API is not supported for extension ${extensionId}`);
141+
}
142+
143+
return () => that.shutdown();
144+
}
135145
});
136146
}
137147
static createApiKernel(
@@ -178,6 +188,26 @@ class WrappedKernelPerExtension extends DisposableBase implements Kernel {
178188
}
179189
}
180190

191+
async shutdown(): Promise<void> {
192+
await this.checkAccess();
193+
logger.debug(`Shutting down kernel ${this.kernel.id} via API for extension ${this.extensionId}`);
194+
try {
195+
// First shutdown the kernel
196+
await this.kernel.shutdown();
197+
} catch (ex) {
198+
logger.error(`Failed to shutdown kernel ${this.kernel.id} for extension ${this.extensionId}`, ex);
199+
throw ex;
200+
}
201+
202+
try {
203+
// Then dispose the kernel
204+
await this.kernel.dispose();
205+
} catch (ex) {
206+
logger.error(`Failed to dispose kernel ${this.kernel.id} for extension ${this.extensionId}`, ex);
207+
throw ex;
208+
}
209+
}
210+
181211
async *executeCodeInternal(
182212
code: string,
183213
handlers: Record<string, (...data: any[]) => Promise<any>> = {},

src/standalone/api/kernels/kernel.unit.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { dispose } from '../../../platform/common/utils/lifecycle';
2222
import { ServiceContainer } from '../../../platform/ioc/container';
2323
import {
2424
IKernelProvider,
25+
IKernelSession,
2526
INotebookKernelExecution,
2627
KernelConnectionMetadata,
2728
type IKernel
@@ -30,6 +31,9 @@ import { createMockedNotebookDocument } from '../../../test/datascience/editor-i
3031
import { IControllerRegistration, IVSCodeNotebookController } from '../../../notebooks/controllers/types';
3132
import { createKernelApiForExtension } from './kernel';
3233
import { noop } from '../../../test/core';
34+
import { JVSC_EXTENSION_ID_FOR_TESTS } from '../../../test/constants';
35+
import { IKernelConnection } from '@jupyterlab/services/lib/kernel/kernel';
36+
import { NotebookCellOutput } from 'vscode';
3337

3438
suite('Kernel Api', () => {
3539
let disposables: IDisposable[] = [];
@@ -80,9 +84,13 @@ suite('Kernel Api', () => {
8084
when(kernelConnection.kind).thenReturn('connectToLiveRemoteKernel');
8185
kernel = mock<IKernel>();
8286
when(kernel.kernelConnectionMetadata).thenReturn(instance(kernelConnection));
87+
when(kernel.disposed).thenReturn(false);
8388
when(kernel.startedAtLeastOnce).thenReturn(true);
8489
notebook = createMockedNotebookDocument([]);
8590
when(kernel.notebook).thenReturn(notebook);
91+
const kernelSession = mock<IKernelSession>();
92+
when(kernel.session).thenReturn(instance(kernelSession));
93+
when(kernelSession.kernel).thenReturn(instance(mock<IKernelConnection>()));
8694

8795
const controllerRegistration = mock<IControllerRegistration>();
8896
when(serviceContainer.get<IControllerRegistration>(IControllerRegistration)).thenReturn(
@@ -92,6 +100,10 @@ suite('Kernel Api', () => {
92100
const controller = mock<NotebookController>();
93101
const execution = mock<INotebookKernelExecution>();
94102
const kernelProvider = mock<IKernelProvider>();
103+
when(execution.executeCode(anything(), anything(), anything(), anything())).thenCall(async function* () {
104+
// Yield a dummy NotebookCellOutput to match the expected type
105+
yield new NotebookCellOutput([]);
106+
});
95107
when(kernelProvider.getKernelExecution(instance(kernel))).thenReturn(instance(execution));
96108
when(serviceContainer.get<IKernelProvider>(IKernelProvider)).thenReturn(instance(kernelProvider));
97109
when(vscController.controller).thenReturn(instance(controller));
@@ -110,4 +122,17 @@ suite('Kernel Api', () => {
110122
assert.equal(ex.name, 'vscode.jupyter.apiAccessRevoked');
111123
}
112124
});
125+
test('Verify Kernel Shutdown', async () => {
126+
when(kernel.status).thenReturn('idle');
127+
when(kernel.shutdown()).thenResolve();
128+
when(kernel.dispose()).thenCall(() => when(kernel.status).thenReturn('dead'));
129+
130+
const { api } = createKernelApiForExtension(JVSC_EXTENSION_ID_FOR_TESTS, instance(kernel));
131+
for await (const _ of api.executeCode('bogus', token)) {
132+
//
133+
}
134+
assert.equal(api.status, 'idle');
135+
await api.shutdown();
136+
assert.equal(api.status, 'dead');
137+
});
113138
});

0 commit comments

Comments
 (0)