Skip to content

Commit c9d35c6

Browse files
authored
Fix exit scene behavior (when exit is anticipated) (#48)
* Detect errors on Manim startup * Wait for exit * Revert "Wait for exit" This reverts commit d6e3916. * Only react to data in active shell and reset on exit * Fix stale progress notification & improve exit mechanism * Remove leftover error class * Add comment explaining early return * Replace return by continue 🙄 * Remove left-over line from merge conflict * Add debug statement back in * Add two more debug statements * Add warn statement when selection is undefined * Propagate async nature of notification messages to caller * Add missing return * Add debug statement to hasActiveShell * Fix try-catch error handling * Rework exit mechanism (force-quit) * Add comment explaining supplementary reset * Send additional keyboard interrupt before quitting scene * Store temp reference to last active shell
1 parent 1b16758 commit c9d35c6

File tree

5 files changed

+90
-25
lines changed

5 files changed

+90
-25
lines changed

src/extension.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as vscode from 'vscode';
22
import { window } from 'vscode';
3-
import { ManimShell } from './manimShell';
3+
import { ManimShell, NoActiveShellError } from './manimShell';
44
import { ManimCell } from './manimCell';
55
import { ManimCellRanges } from './manimCellRanges';
66
import { previewCode } from './previewCode';
@@ -152,8 +152,13 @@ async function previewSelection() {
152152
async function clearScene() {
153153
try {
154154
await ManimShell.instance.executeCommandErrorOnNoActiveSession("clear()");
155-
} catch (NoActiveSessionError) {
156-
Window.showErrorMessage('No active Manim session found to remove objects from.');
155+
} catch (error) {
156+
if (error instanceof NoActiveShellError) {
157+
Window.showErrorMessage('No active Manim session found to remove objects from.');
158+
} else {
159+
Logger.error(`💥 Error while trying to remove objects from scene: ${error}`);
160+
throw error;
161+
}
157162
}
158163
}
159164

src/logger.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -161,19 +161,19 @@ export class Logger {
161161
*/
162162
export class Window {
163163

164-
public static showInformationMessage(message: string) {
164+
public static async showInformationMessage(message: string) {
165165
Logger.info(`💡 ${message}`);
166-
window.showInformationMessage(message);
166+
return await window.showInformationMessage(message);
167167
}
168168

169-
public static showWarningMessage(message: string, ...items: string[]) {
169+
public static async showWarningMessage(message: string, ...items: string[]) {
170170
Logger.warn(`💡 ${message}`);
171-
window.showWarningMessage(message, ...items);
171+
return await window.showWarningMessage(message, ...items);
172172
}
173173

174-
public static showErrorMessage(message: string) {
174+
public static async showErrorMessage(message: string) {
175175
Logger.error(`💡 ${message}`);
176-
window.showErrorMessage(message);
176+
return await window.showErrorMessage(message);
177177
}
178178
}
179179

src/manimShell.ts

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,12 @@ enum ManimShellEvent {
5959
* execution has ended before we have detected the start of the ManimGL
6060
* session.
6161
*/
62-
MANIM_NOT_STARTED = 'manimglNotStarted'
62+
MANIM_NOT_STARTED = 'manimglNotStarted',
63+
64+
/**
65+
* Event emitted when the active shell is reset.
66+
*/
67+
RESET = 'reset'
6368
}
6469

6570
/**
@@ -81,6 +86,12 @@ export interface CommandExecutionEventHandler {
8186
* of ANSI control codes.
8287
*/
8388
onData?: (data: string) => void;
89+
90+
/**
91+
* Callback that is invoked when the manim shell is reset. This indicates
92+
* that the event handler should clean up any resources.
93+
*/
94+
onReset?: () => void;
8495
}
8596

8697
/**
@@ -263,6 +274,8 @@ export class ManimShell {
263274

264275
this.isExecutingCommand = true;
265276
Logger.debug("🔒 Command execution locked");
277+
const resetListener = () => { handler?.onReset?.(); };
278+
this.eventEmitter.on(ManimShellEvent.RESET, resetListener);
266279

267280
let shell: Terminal;
268281
if (errorOnNoActiveShell) {
@@ -285,6 +298,7 @@ export class ManimShell {
285298
Logger.debug("🔓 Command execution unlocked");
286299
this.isExecutingCommand = false;
287300
this.eventEmitter.off(ManimShellEvent.DATA, dataListener);
301+
this.eventEmitter.off(ManimShellEvent.RESET, resetListener);
288302
});
289303
if (waitUntilFinished) {
290304
Logger.debug(`🕒 Waiting until command has finished: ${command}`);
@@ -317,12 +331,14 @@ export class ManimShell {
317331
const shouldAsk = await vscode.workspace.getConfiguration("manim-notebook")
318332
.get("confirmKillingActiveSceneToStartNewOne");
319333
if (shouldAsk) {
334+
Logger.debug("🔆 Active shell found, asking user to kill active scene");
320335
if (!await this.doesUserWantToKillActiveScene()) {
336+
Logger.debug("🔆 User didn't want to kill active scene");
321337
return;
322338
}
323339
}
324340
Logger.debug("🔆 User confirmed to kill active scene");
325-
exitScene();
341+
await this.forceQuitActiveShell();
326342
}
327343
this.activeShell = window.createTerminal();
328344
} else {
@@ -361,12 +377,39 @@ export class ManimShell {
361377
this.iPythonCellCount = 0;
362378
this.activeShell = null;
363379
this.shellWeTryToSpawnIn = null;
380+
this.eventEmitter.emit(ManimShellEvent.RESET);
364381
this.eventEmitter.removeAllListeners();
365382
}
366383

384+
/**
385+
* Forces the terminal to quit and thus the ManimGL session to end.
386+
*
387+
* Beforehand, this was implemented more gracefully by sending a keyboard
388+
* interrupt (`Ctrl+C`), followed by the `exit()` command in the IPython
389+
* session. However, on MacOS, the keyboard interrupt itself already exits
390+
* from the entire IPython session and does not just interrupt the current
391+
* running command (inside IPython) as would be expected.
392+
* See https://github.com/3b1b/manim/discussions/2236
393+
*/
394+
public async forceQuitActiveShell() {
395+
if (this.activeShell) {
396+
Logger.debug("🔚 Force-quitting active shell");
397+
let lastActiveShell = this.activeShell;
398+
await this.sendKeyboardInterrupt();
399+
lastActiveShell?.dispose();
400+
// This is also taken care of when we detect that the shell has ended
401+
// in the `onDidEndTerminalShellExecution` event handler. However,
402+
// it doesn't harm to reset the active shell here as well just to
403+
// be sure.
404+
this.resetActiveShell();
405+
} else {
406+
Logger.debug("🔚 No active shell found to force quit");
407+
}
408+
}
409+
367410
/**
368411
* Ask the user if they want to kill the active scene. Might modify the
369-
* setting that control if the user should be asked in the future.
412+
* setting that controls if the user should be asked in the future.
370413
*
371414
* @returns true if the user wants to kill the active scene, false otherwise.
372415
*/
@@ -377,6 +420,9 @@ export class ManimShell {
377420
const selection = await Window.showWarningMessage(
378421
"We need to kill your Manim session to spawn a new one.",
379422
"Kill it", KILL_IT_ALWAYS_OPTION, CANCEL_OPTION);
423+
if (selection === undefined) {
424+
Logger.warn("❌ User selection undefined, but shouldn't be");
425+
}
380426
if (selection === undefined || selection === CANCEL_OPTION) {
381427
return false;
382428
}
@@ -398,7 +444,10 @@ export class ManimShell {
398444
* Manim session (IPython environment), is considered inactive.
399445
*/
400446
private hasActiveShell(): boolean {
401-
return this.activeShell !== null && this.activeShell.exitStatus === undefined;
447+
const hasActiveShell =
448+
this.activeShell !== null && this.activeShell.exitStatus === undefined;
449+
Logger.debug(`👩‍💻 Has active shell?: ${hasActiveShell}`);
450+
return hasActiveShell;
402451
}
403452

404453
/**
@@ -492,7 +541,8 @@ export class ManimShell {
492541

493542
private async sendKeyboardInterrupt() {
494543
Logger.debug("💨 Sending keyboard interrupt to terminal");
495-
this.activeShell?.sendText('\x03'); // send `Ctrl+C`
544+
await this.activeShell?.sendText('\x03'); // send `Ctrl+C`
545+
await new Promise(resolve => setTimeout(resolve, 250));
496546
}
497547

498548
/**
@@ -514,17 +564,26 @@ export class ManimShell {
514564
for await (const data of withoutAnsiCodes(stream)) {
515565
Logger.trace(`🧾 Terminal data:\n${data}`);
516566

517-
this.eventEmitter.emit(ManimShellEvent.DATA, data);
518-
519567
if (data.match(MANIM_WELCOME_REGEX)) {
568+
// Manim detected in new terminal
520569
if (this.activeShell && this.activeShell !== event.terminal) {
521570
Logger.debug("👋 Manim detected in new terminal, exiting old scene");
522-
exitScene();
571+
await this.forceQuitActiveShell();
523572
}
524573
Logger.debug("👋 Manim welcome string detected");
525574
this.activeShell = event.terminal;
526575
}
527576

577+
// Subsequent data handling should only occur for the
578+
// currently active shell. This is important during
579+
// overlapping commands, e.g. when one shell is exited
580+
// and another one started.
581+
if (this.activeShell !== event.terminal) {
582+
continue;
583+
}
584+
585+
this.eventEmitter.emit(ManimShellEvent.DATA, data);
586+
528587
if (data.match(KEYBOARD_INTERRUPT_REGEX)) {
529588
Logger.debug("🛑 Keyboard interrupt detected");
530589
this.eventEmitter.emit(ManimShellEvent.KEYBOARD_INTERRUPT);

src/previewCode.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ export async function previewCode(code: string, startLine: number): Promise<void
4141
},
4242
onData: (data) => {
4343
progress?.reportOnData(data);
44+
},
45+
onReset: () => {
46+
progress?.finish();
4447
}
4548
});
4649
} finally {

src/startStopScene.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as vscode from 'vscode';
2-
import { ManimShell } from './manimShell';
2+
import { ManimShell, NoActiveShellError } from './manimShell';
33
import { window } from 'vscode';
44
import { Logger, Window } from './logger';
55

@@ -105,13 +105,11 @@ export async function startScene(lineStart?: number) {
105105
}
106106

107107
/**
108-
* Runs the `exit()` command in the terminal to close the animation window
109-
* and the IPython terminal.
108+
* Force-quits the active Manim session by disposing the respective VSCode
109+
* terminal that is currently hosting the session.
110+
*
111+
* See `forceQuitActiveShell()` for more details.
110112
*/
111113
export async function exitScene() {
112-
try {
113-
await ManimShell.instance.executeCommandErrorOnNoActiveSession("exit()", false, true);
114-
} catch (NoActiveSessionError) {
115-
Window.showErrorMessage('No active Manim session found to exit.');
116-
}
114+
await ManimShell.instance.forceQuitActiveShell();
117115
}

0 commit comments

Comments
 (0)