Skip to content

Commit e0476b5

Browse files
authored
Allow double (or more) interrupts (#12278)
* Allow double (or more) interrupts * Fix other tests that were checking previous allowed state
1 parent 39ad45a commit e0476b5

File tree

10 files changed

+68
-20
lines changed

10 files changed

+68
-20
lines changed

news/2 Fixes/10587.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow interrupt to happen more than once in a notebook or the interactive window.

src/client/datascience/interactive-common/interactiveBase.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -430,8 +430,6 @@ export abstract class InteractiveBase extends WebViewHost<IInteractiveWindowMapp
430430
@captureTelemetry(Telemetry.Interrupt)
431431
public async interruptKernel(): Promise<void> {
432432
if (this._notebook && !this.restartingKernel) {
433-
this.restartingKernel = true;
434-
435433
const status = this.statusProvider.set(
436434
localize.DataScience.interruptKernelStatus(),
437435
true,
@@ -448,7 +446,7 @@ export abstract class InteractiveBase extends WebViewHost<IInteractiveWindowMapp
448446
status.dispose();
449447

450448
// We timed out, ask the user if they want to restart instead.
451-
if (result === InterruptResult.TimedOut) {
449+
if (result === InterruptResult.TimedOut && !this.restartingKernel) {
452450
const message = localize.DataScience.restartKernelAfterInterruptMessage();
453451
const yes = localize.DataScience.restartKernelMessageYes();
454452
const no = localize.DataScience.restartKernelMessageNo();
@@ -464,8 +462,6 @@ export abstract class InteractiveBase extends WebViewHost<IInteractiveWindowMapp
464462
status.dispose();
465463
traceError(err);
466464
this.applicationShell.showErrorMessage(err);
467-
} finally {
468-
this.restartingKernel = false;
469465
}
470466
}
471467
}

src/client/datascience/jupyter/jupyterNotebook.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,12 @@ export class JupyterNotebookBase implements INotebook {
167167
public get onKernelRestarted(): Event<void> {
168168
return this.kernelRestarted.event;
169169
}
170+
171+
public get onKernelInterrupted(): Event<void> {
172+
return this.kernelInterrupted.event;
173+
}
170174
private readonly kernelRestarted = new EventEmitter<void>();
175+
private readonly kernelInterrupted = new EventEmitter<void>();
171176
private disposed = new EventEmitter<void>();
172177
private sessionStatusChanged: Disposable | undefined;
173178
private initializedMatplotlib = false;
@@ -522,6 +527,9 @@ export class JupyterNotebookBase implements INotebook {
522527
// Cancel all other pending cells as we interrupted.
523528
this.finishUncompletedCells();
524529

530+
// Fire event that we interrupted.
531+
this.kernelInterrupted.fire();
532+
525533
// Indicate the interrupt worked.
526534
return InterruptResult.Success;
527535
} catch (exc) {

src/client/datascience/jupyter/liveshare/guestJupyterNotebook.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export class GuestJupyterNotebook
7272
IJupyterKernelSpec | LiveKernelModel
7373
>().event;
7474
public onKernelRestarted = new EventEmitter<void>().event;
75+
public onKernelInterrupted = new EventEmitter<void>().event;
7576
public onDisposed = new EventEmitter<void>().event;
7677
private _jupyterLab?: typeof import('@jupyterlab/services');
7778
private responseQueue: ResponseQueue = new ResponseQueue();

src/client/datascience/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ export interface INotebook extends IAsyncDisposable {
182182
onDisposed: Event<void>;
183183
onKernelChanged: Event<IJupyterKernelSpec | LiveKernelModel>;
184184
onKernelRestarted: Event<void>;
185+
onKernelInterrupted: Event<void>;
185186
clear(id: string): void;
186187
executeObservable(code: string, file: string, line: number, id: string, silent: boolean): Observable<ICell[]>;
187188
execute(

src/datascience-ui/history-react/interactivePanel.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,6 @@ ${buildSettingsCss(this.props.settings)}`}</style>
136136
<ImageButton
137137
baseTheme={this.props.baseTheme}
138138
onClick={this.props.restartKernel}
139-
disabled={this.props.busy}
140139
tooltip={getLocString('DataScience.restartServer', 'Restart IPython kernel')}
141140
>
142141
<Image
@@ -148,7 +147,6 @@ ${buildSettingsCss(this.props.settings)}`}</style>
148147
<ImageButton
149148
baseTheme={this.props.baseTheme}
150149
onClick={this.props.interruptKernel}
151-
disabled={this.props.busy}
152150
tooltip={getLocString('DataScience.interruptKernel', 'Interrupt IPython kernel')}
153151
>
154152
<Image

src/datascience-ui/native-editor/toolbar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ export class Toolbar extends React.PureComponent<INativeEditorToolbarProps> {
160160
<ImageButton
161161
baseTheme={this.props.baseTheme}
162162
onClick={this.props.restartKernel}
163-
disabled={this.props.busy || !canRestartAndInterruptKernel}
163+
disabled={!canRestartAndInterruptKernel}
164164
className="native-button"
165165
tooltip={getLocString('DataScience.restartServer', 'Restart IPython kernel')}
166166
>
@@ -173,7 +173,7 @@ export class Toolbar extends React.PureComponent<INativeEditorToolbarProps> {
173173
<ImageButton
174174
baseTheme={this.props.baseTheme}
175175
onClick={this.props.interruptKernel}
176-
disabled={this.props.busy || !canRestartAndInterruptKernel}
176+
disabled={!canRestartAndInterruptKernel}
177177
className="native-button"
178178
tooltip={getLocString('DataScience.interruptKernel', 'Interrupt IPython kernel')}
179179
>

src/test/datascience/interactiveWindow.functional.test.tsx

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { Disposable, Selection, TextDocument, TextEditor, Uri } from 'vscode';
1212
import { ReactWrapper } from 'enzyme';
1313
import { IApplicationShell, IDocumentManager } from '../../client/common/application/types';
1414
import { IDataScienceSettings } from '../../client/common/types';
15-
import { createDeferred, waitForPromise } from '../../client/common/utils/async';
15+
import { createDeferred, sleep, waitForPromise } from '../../client/common/utils/async';
1616
import { noop } from '../../client/common/utils/misc';
1717
import { EXTENSION_ROOT_DIR } from '../../client/constants';
1818
import { generateCellsFromDocument } from '../../client/datascience/cellFactory';
@@ -555,6 +555,52 @@ Type: builtin_function_or_method`,
555555
}
556556
);
557557

558+
const interruptCode = `
559+
import time
560+
for i in range(0, 100):
561+
try:
562+
time.sleep(0.5)
563+
except KeyboardInterrupt:
564+
time.sleep(0.5)`;
565+
566+
runMountedTest(
567+
'Interrupt double',
568+
async (wrapper) => {
569+
let interruptedKernel = false;
570+
const interactiveWindow = await getOrCreateInteractiveWindow(ioc);
571+
await interactiveWindow.show();
572+
interactiveWindow.notebook?.onKernelInterrupted(() => (interruptedKernel = true));
573+
574+
let timerCount = 0;
575+
addContinuousMockData(ioc, interruptCode, async (_c) => {
576+
timerCount += 1;
577+
await sleep(0.5);
578+
return Promise.resolve({ result: '', haveMore: timerCount < 100 });
579+
});
580+
581+
addMockData(ioc, interruptCode, undefined, 'text/plain');
582+
583+
// Run the interrupt code and then interrupt it twice to make sure we can interrupt twice
584+
const waitForAdd = addCode(ioc, wrapper, interruptCode);
585+
586+
// 'Click' the button in the react control. We need to verify we can
587+
// click it more than once.
588+
const interrupt = findButton(wrapper, InteractivePanel, 4);
589+
interrupt?.simulate('click');
590+
await sleep(0.1);
591+
interrupt?.simulate('click');
592+
593+
// We should get out of the wait for add
594+
await waitForAdd;
595+
596+
// We should have also fired an interrupt
597+
assert.ok(interruptedKernel, 'Kernel was not interrupted');
598+
},
599+
() => {
600+
return ioc;
601+
}
602+
);
603+
558604
runMountedTest(
559605
'Export',
560606
async (wrapper) => {

src/test/datascience/mockJupyterNotebook.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ export class MockJupyterNotebook implements INotebook {
5151
>().event;
5252
public onDisposed = new EventEmitter<void>().event;
5353
public onKernelRestarted = new EventEmitter<void>().event;
54+
public get onKernelInterrupted(): Event<void> {
55+
return this.kernelInterrupted.event;
56+
}
57+
private kernelInterrupted = new EventEmitter<void>();
5458
private onStatusChangedEvent: EventEmitter<ServerStatus> | undefined;
5559

5660
constructor(private providerConnection: INotebookProviderConnection | undefined) {

src/test/datascience/nativeEditor.toolbar.functional.test.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -176,13 +176,6 @@ suite('DataScience Native Toolbar', () => {
176176
suite('Restart & Interrupt Kernel', () => {
177177
getNamesAndValues<ServerStatus>(ServerStatus).forEach((status) => {
178178
// Should always be disabled if busy.
179-
test(`If Kernel status is ${status.name} and busy, both are disabled`, () => {
180-
props.kernel.jupyterServerStatus = status.name as any;
181-
props.busy = true;
182-
mountToolbar();
183-
assertDisabled(Button.RestartKernel);
184-
assertDisabled(Button.InterruptKernel);
185-
});
186179
if (status.name === ServerStatus.NotStarted) {
187180
// Should be disabled if not busy and status === 'Not Started'.
188181
test(`If Kernel status is ${ServerStatus.NotStarted} and not busy, both are disabled`, () => {
@@ -193,10 +186,10 @@ suite('DataScience Native Toolbar', () => {
193186
assertDisabled(Button.InterruptKernel);
194187
});
195188
} else {
196-
// Should be enabled if not busy and status != 'Not Started'.
197-
test(`If Kernel status is ${status.name} and not busy, both are enabled`, () => {
189+
// Should be enabled if busy and status != 'Not Started'.
190+
test(`If Kernel status is ${status.name}, both are enabled`, () => {
198191
props.kernel.jupyterServerStatus = status.name as any;
199-
props.busy = false;
192+
props.busy = true;
200193
mountToolbar();
201194
assertEnabled(Button.RestartKernel);
202195
assertEnabled(Button.InterruptKernel);

0 commit comments

Comments
 (0)