kernel-checkpoint 0.1.7__tar.gz → 0.1.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/PKG-INFO +1 -1
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/jupyter-notebook-image/Dockerfile +1 -1
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/kernel_checkpoint/_version.py +1 -1
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/kernel_checkpoint/labextension/build_log.json +1 -1
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/kernel_checkpoint/labextension/package.json +2 -2
- kernel_checkpoint-0.1.7/kernel_checkpoint/labextension/static/lib_index_js.3571ff1d8cbf7ea37b2e.js → kernel_checkpoint-0.1.8/kernel_checkpoint/labextension/static/lib_index_js.6dc5fa8b4a9bc62a809d.js +429 -9
- kernel_checkpoint-0.1.8/kernel_checkpoint/labextension/static/lib_index_js.6dc5fa8b4a9bc62a809d.js.map +1 -0
- kernel_checkpoint-0.1.7/kernel_checkpoint/labextension/static/remoteEntry.900312eadd6aa5546ed7.js → kernel_checkpoint-0.1.8/kernel_checkpoint/labextension/static/remoteEntry.5ffc549e28b042648bca.js +3 -3
- kernel_checkpoint-0.1.7/kernel_checkpoint/labextension/static/remoteEntry.900312eadd6aa5546ed7.js.map → kernel_checkpoint-0.1.8/kernel_checkpoint/labextension/static/remoteEntry.5ffc549e28b042648bca.js.map +1 -1
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/package.json +1 -1
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/src/checkpoint-panel.tsx +13 -3
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/src/index.ts +102 -4
- kernel_checkpoint-0.1.8/src/restore-handler.ts +418 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/src/types.ts +21 -1
- kernel_checkpoint-0.1.7/kernel_checkpoint/labextension/static/lib_index_js.3571ff1d8cbf7ea37b2e.js.map +0 -1
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/.copier-answers.yml +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/.gitignore +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/.prettierignore +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/.yarnrc.yml +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/4 +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/=4 +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/CHANGELOG.md +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/LICENSE +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/README.md +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/RELEASE.md +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/babel.config.js +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/cursor_jupyterlab_extension_for_checkpo.md +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/install.json +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/jest.config.js +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/jupyter-config/jupyter_server_config.d/kernel_checkpoint.json +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/kernel_checkpoint/Untitled.ipynb +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/kernel_checkpoint/__init__.py +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/kernel_checkpoint/handlers.py +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/kernel_checkpoint/labextension/static/style.js +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/kernel_checkpoint/labextension/static/style_index_js.c3e72438ed03e9dee0fc.js +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/kernel_checkpoint/labextension/static/style_index_js.c3e72438ed03e9dee0fc.js.map +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/pyproject.toml +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/setup.py +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/src/__tests__/kernel_checkpoint.spec.ts +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/src/api.ts +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/style/base.css +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/style/index.css +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/style/index.js +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/tsconfig.json +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/tsconfig.test.json +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/ui-tests/README.md +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/ui-tests/jupyter_server_test_config.py +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/ui-tests/package.json +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/ui-tests/playwright.config.js +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/ui-tests/tests/kernel_checkpoint.spec.ts +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/ui-tests/yarn.lock +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/yarn.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kernel_checkpoint
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Summary: An extension for saving and restoring kernel pod state with jupyter enterprise gateway
|
|
5
5
|
Project-URL: Homepage, https://github.com/lehuannhatrang/kernel-checkpoint-extension.git
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/lehuannhatrang/kernel-checkpoint-extension.git/issues
|
{kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/kernel_checkpoint/labextension/package.json
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kernel-checkpoint",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "An extension for saving and restoring kernel pod state with jupyter enterprise gateway",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
},
|
|
115
115
|
"outputDir": "kernel_checkpoint/labextension",
|
|
116
116
|
"_build": {
|
|
117
|
-
"load": "static/remoteEntry.
|
|
117
|
+
"load": "static/remoteEntry.5ffc549e28b042648bca.js",
|
|
118
118
|
"extension": "./extension",
|
|
119
119
|
"style": "./style"
|
|
120
120
|
}
|
|
@@ -119,7 +119,7 @@ function formatDate(iso) {
|
|
|
119
119
|
return new Date(iso).toLocaleString();
|
|
120
120
|
}
|
|
121
121
|
function CheckpointPanel(props) {
|
|
122
|
-
const { namespace, kernelId, kernelSpecName, notebookName, onRestore } = props;
|
|
122
|
+
const { namespace, kernelId, kernelSpecName, notebookName, onRestore, onBeforeCreate } = props;
|
|
123
123
|
const [checkpoints, setCheckpoints] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]);
|
|
124
124
|
const [loading, setLoading] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(true);
|
|
125
125
|
const [error, setError] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
|
|
@@ -170,6 +170,7 @@ function CheckpointPanel(props) {
|
|
|
170
170
|
setCreating(true);
|
|
171
171
|
setFlash(null);
|
|
172
172
|
try {
|
|
173
|
+
const busyCells = onBeforeCreate ? onBeforeCreate() : [];
|
|
173
174
|
await _api__WEBPACK_IMPORTED_MODULE_1__.CheckpointAPI.createCheckpoint({
|
|
174
175
|
name: newName.trim(),
|
|
175
176
|
namespace,
|
|
@@ -178,7 +179,8 @@ function CheckpointPanel(props) {
|
|
|
178
179
|
metadata: {
|
|
179
180
|
kernelId,
|
|
180
181
|
kernelName: kernelSpecName,
|
|
181
|
-
notebookName
|
|
182
|
+
notebookName,
|
|
183
|
+
busyCells
|
|
182
184
|
}
|
|
183
185
|
});
|
|
184
186
|
setNewName('');
|
|
@@ -236,7 +238,7 @@ function CheckpointPanel(props) {
|
|
|
236
238
|
}
|
|
237
239
|
};
|
|
238
240
|
const handleRestore = async (checkpointName) => {
|
|
239
|
-
var _a, _b, _c, _d;
|
|
241
|
+
var _a, _b, _c, _d, _e, _f;
|
|
240
242
|
setConfirmRestore(null);
|
|
241
243
|
setRestoring(checkpointName);
|
|
242
244
|
setFlash(null);
|
|
@@ -250,7 +252,8 @@ function CheckpointPanel(props) {
|
|
|
250
252
|
if (!cpKernelId) {
|
|
251
253
|
throw new Error('No kernel ID found in checkpoint metadata');
|
|
252
254
|
}
|
|
253
|
-
|
|
255
|
+
const busyCells = (_e = (_d = detail.metadata) === null || _d === void 0 ? void 0 : _d.busyCells) !== null && _e !== void 0 ? _e : [];
|
|
256
|
+
await onRestore(checkpointName, checkpointFile.storagePath, checkpointFile.containerName, cpKernelId, busyCells);
|
|
254
257
|
setFlash({
|
|
255
258
|
type: 'success',
|
|
256
259
|
text: `Kernel restored from "${checkpointName}". The kernel is restarting.`
|
|
@@ -259,7 +262,7 @@ function CheckpointPanel(props) {
|
|
|
259
262
|
catch (err) {
|
|
260
263
|
setFlash({
|
|
261
264
|
type: 'error',
|
|
262
|
-
text: (
|
|
265
|
+
text: (_f = err.message) !== null && _f !== void 0 ? _f : 'Failed to restore checkpoint'
|
|
263
266
|
});
|
|
264
267
|
}
|
|
265
268
|
finally {
|
|
@@ -389,6 +392,8 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
389
392
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_4__);
|
|
390
393
|
/* harmony import */ var _checkpoint_panel__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./checkpoint-panel */ "./lib/checkpoint-panel.js");
|
|
391
394
|
/* harmony import */ var _api__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./api */ "./lib/api.js");
|
|
395
|
+
/* harmony import */ var _restore_handler__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./restore-handler */ "./lib/restore-handler.js");
|
|
396
|
+
|
|
392
397
|
|
|
393
398
|
|
|
394
399
|
|
|
@@ -418,6 +423,29 @@ const plugin = {
|
|
|
418
423
|
requires: [_jupyterlab_notebook__WEBPACK_IMPORTED_MODULE_0__.INotebookTracker],
|
|
419
424
|
activate: (app, notebookTracker) => {
|
|
420
425
|
console.log('JupyterLab extension kernel-checkpoint is activated!');
|
|
426
|
+
// ── Per-notebook execution tracking ──────────────────────────────
|
|
427
|
+
//
|
|
428
|
+
// Each open notebook gets its own CellExecutionTracker that
|
|
429
|
+
// continuously records in-flight msg_id → execution_count pairs
|
|
430
|
+
// from the kernel's iopub channel. At checkpoint time this map
|
|
431
|
+
// is consulted to capture the execution counts for busy cells.
|
|
432
|
+
const trackerMap = new Map();
|
|
433
|
+
function ensureTracker(panel) {
|
|
434
|
+
let cellTracker = trackerMap.get(panel.id);
|
|
435
|
+
if (!cellTracker) {
|
|
436
|
+
cellTracker = new _restore_handler__WEBPACK_IMPORTED_MODULE_7__.CellExecutionTracker();
|
|
437
|
+
trackerMap.set(panel.id, cellTracker);
|
|
438
|
+
}
|
|
439
|
+
return cellTracker;
|
|
440
|
+
}
|
|
441
|
+
function connectTrackerToKernel(panel) {
|
|
442
|
+
var _a;
|
|
443
|
+
const kernel = (_a = panel.sessionContext.session) === null || _a === void 0 ? void 0 : _a.kernel;
|
|
444
|
+
if (kernel) {
|
|
445
|
+
ensureTracker(panel).connectToKernel(kernel);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
// ── Command ──────────────────────────────────────────────────────
|
|
421
449
|
app.commands.addCommand(COMMAND_ID, {
|
|
422
450
|
label: 'Saving Points',
|
|
423
451
|
caption: 'Manage kernel checkpoints',
|
|
@@ -461,7 +489,13 @@ const plugin = {
|
|
|
461
489
|
const kernelId = kernel.id;
|
|
462
490
|
const kernelSpecName = kernel.name || 'python_kubernetes';
|
|
463
491
|
const notebookName = panel.title.label || '';
|
|
464
|
-
|
|
492
|
+
// ── Restore callback ─────────────────────────────────────────
|
|
493
|
+
//
|
|
494
|
+
// Called by the CheckpointPanel when the user confirms a restore.
|
|
495
|
+
// Creates a new kernel seeded with the checkpoint environment
|
|
496
|
+
// variables, switches the notebook session to it, and then
|
|
497
|
+
// activates the RestoreHandler to catch orphaned iopub messages.
|
|
498
|
+
const onRestore = async (checkpointName, checkpointFilePath, containerName, cpKernelId, busyCells) => {
|
|
465
499
|
const settings = _jupyterlab_services__WEBPACK_IMPORTED_MODULE_3__.ServerConnection.makeSettings();
|
|
466
500
|
const kernelUrl = _jupyterlab_coreutils__WEBPACK_IMPORTED_MODULE_2__.URLExt.join(settings.baseUrl, 'api', 'kernels');
|
|
467
501
|
const response = await _jupyterlab_services__WEBPACK_IMPORTED_MODULE_3__.ServerConnection.makeRequest(kernelUrl, {
|
|
@@ -483,13 +517,37 @@ const plugin = {
|
|
|
483
517
|
}
|
|
484
518
|
const kernelData = await response.json();
|
|
485
519
|
await sessionContext.changeKernel({ id: kernelData.id });
|
|
520
|
+
// ── Activate Catch-and-Render (Steps 3-6) ──────────────────
|
|
521
|
+
//
|
|
522
|
+
// The restored kernel is now connected. If there were busy
|
|
523
|
+
// cells at checkpoint time, spin up a RestoreHandler that
|
|
524
|
+
// hooks iopub and intercepts orphaned messages.
|
|
525
|
+
if (busyCells.length > 0) {
|
|
526
|
+
console.log(`[kernel-checkpoint] Restore detected ${busyCells.length} ` +
|
|
527
|
+
'busy cell(s). Activating RestoreHandler.');
|
|
528
|
+
const handler = new _restore_handler__WEBPACK_IMPORTED_MODULE_7__.RestoreHandler();
|
|
529
|
+
handler.hookRestoredKernel(panel, busyCells);
|
|
530
|
+
}
|
|
486
531
|
};
|
|
532
|
+
// ── Dialog construction ──────────────────────────────────────
|
|
487
533
|
const body = new CheckpointDialogBody({
|
|
488
534
|
namespace: config.namespace,
|
|
489
535
|
kernelId,
|
|
490
536
|
kernelSpecName,
|
|
491
537
|
notebookName,
|
|
492
|
-
onRestore
|
|
538
|
+
onRestore,
|
|
539
|
+
onBeforeCreate: () => {
|
|
540
|
+
const cellTracker = trackerMap.get(panel.id);
|
|
541
|
+
if (!cellTracker) {
|
|
542
|
+
return [];
|
|
543
|
+
}
|
|
544
|
+
const busyCells = (0,_restore_handler__WEBPACK_IMPORTED_MODULE_7__.scanBusyCells)(panel, cellTracker);
|
|
545
|
+
if (busyCells.length > 0) {
|
|
546
|
+
console.log(`[kernel-checkpoint] Scanned ${busyCells.length} busy cell(s) ` +
|
|
547
|
+
'before checkpoint:', busyCells);
|
|
548
|
+
}
|
|
549
|
+
return busyCells;
|
|
550
|
+
}
|
|
493
551
|
});
|
|
494
552
|
await (0,_jupyterlab_apputils__WEBPACK_IMPORTED_MODULE_1__.showDialog)({
|
|
495
553
|
title: 'Kernel Checkpoints',
|
|
@@ -499,8 +557,9 @@ const plugin = {
|
|
|
499
557
|
body.dispose();
|
|
500
558
|
}
|
|
501
559
|
});
|
|
502
|
-
//
|
|
560
|
+
// ── Notebook lifecycle wiring ────────────────────────────────────
|
|
503
561
|
notebookTracker.widgetAdded.connect((_sender, panel) => {
|
|
562
|
+
// Toolbar button
|
|
504
563
|
const button = new _jupyterlab_apputils__WEBPACK_IMPORTED_MODULE_1__.ToolbarButton({
|
|
505
564
|
label: 'Saving Points',
|
|
506
565
|
tooltip: 'Open kernel checkpoint manager',
|
|
@@ -509,13 +568,374 @@ const plugin = {
|
|
|
509
568
|
}
|
|
510
569
|
});
|
|
511
570
|
panel.toolbar.insertItem(10, 'kernel-checkpoint', button);
|
|
571
|
+
// Execution tracker setup — connect once the session is ready,
|
|
572
|
+
// and reconnect whenever the kernel is swapped.
|
|
573
|
+
panel.sessionContext.ready.then(() => {
|
|
574
|
+
connectTrackerToKernel(panel);
|
|
575
|
+
});
|
|
576
|
+
panel.sessionContext.kernelChanged.connect(() => {
|
|
577
|
+
connectTrackerToKernel(panel);
|
|
578
|
+
});
|
|
579
|
+
panel.disposed.connect(() => {
|
|
580
|
+
const cellTracker = trackerMap.get(panel.id);
|
|
581
|
+
if (cellTracker) {
|
|
582
|
+
cellTracker.disconnectFromKernel();
|
|
583
|
+
trackerMap.delete(panel.id);
|
|
584
|
+
}
|
|
585
|
+
});
|
|
512
586
|
});
|
|
513
587
|
}
|
|
514
588
|
};
|
|
515
589
|
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (plugin);
|
|
516
590
|
|
|
517
591
|
|
|
592
|
+
/***/ },
|
|
593
|
+
|
|
594
|
+
/***/ "./lib/restore-handler.js"
|
|
595
|
+
/*!********************************!*\
|
|
596
|
+
!*** ./lib/restore-handler.js ***!
|
|
597
|
+
\********************************/
|
|
598
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
599
|
+
|
|
600
|
+
__webpack_require__.r(__webpack_exports__);
|
|
601
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
602
|
+
/* harmony export */ CellExecutionTracker: () => (/* binding */ CellExecutionTracker),
|
|
603
|
+
/* harmony export */ RestoreHandler: () => (/* binding */ RestoreHandler),
|
|
604
|
+
/* harmony export */ loadCheckpointCellMetadata: () => (/* binding */ loadCheckpointCellMetadata),
|
|
605
|
+
/* harmony export */ saveCheckpointCellMetadata: () => (/* binding */ saveCheckpointCellMetadata),
|
|
606
|
+
/* harmony export */ scanBusyCells: () => (/* binding */ scanBusyCells)
|
|
607
|
+
/* harmony export */ });
|
|
608
|
+
/**
|
|
609
|
+
* Safely extracts `msg_id` from a kernel message's `parent_header`.
|
|
610
|
+
* Returns undefined when the parent_header is empty (e.g. unsolicited
|
|
611
|
+
* kernel-status broadcasts at startup).
|
|
612
|
+
*/
|
|
613
|
+
function getParentMsgId(msg) {
|
|
614
|
+
const ph = msg.parent_header;
|
|
615
|
+
return ph && 'msg_id' in ph
|
|
616
|
+
? ph.msg_id
|
|
617
|
+
: undefined;
|
|
618
|
+
}
|
|
619
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
620
|
+
// Mock save / load helpers.
|
|
621
|
+
// These demonstrate the checkpoint-metadata persistence contract.
|
|
622
|
+
// Replace the bodies with real API calls (e.g. CheckpointAPI.updateCheckpoint)
|
|
623
|
+
// when wiring to your backend.
|
|
624
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
625
|
+
async function saveCheckpointCellMetadata(_checkpointName, _namespace, busyCells) {
|
|
626
|
+
console.log(`[RestoreHandler] Would persist ${busyCells.length} busy-cell record(s)`, busyCells);
|
|
627
|
+
}
|
|
628
|
+
async function loadCheckpointCellMetadata(_checkpointName, _namespace) {
|
|
629
|
+
console.log('[RestoreHandler] Would load busy-cell records from backend');
|
|
630
|
+
return [];
|
|
631
|
+
}
|
|
632
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
633
|
+
// CellExecutionTracker
|
|
634
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
635
|
+
/**
|
|
636
|
+
* Continuously monitors a kernel's iopub channel to maintain a live map of
|
|
637
|
+
* **in-flight** execute requests: `msg_id → execution_count`.
|
|
638
|
+
*
|
|
639
|
+
* When the kernel broadcasts `execute_input`, the tracker records the
|
|
640
|
+
* execution count assigned by the kernel (this is the number that will
|
|
641
|
+
* eventually be shown as `[N]` in the cell prompt). When `status: idle`
|
|
642
|
+
* arrives for that same `msg_id`, the entry is removed.
|
|
643
|
+
*
|
|
644
|
+
* The tracker must be connected to the kernel **before** any cells are
|
|
645
|
+
* executed so that the execution count is available at checkpoint time.
|
|
646
|
+
*/
|
|
647
|
+
class CellExecutionTracker {
|
|
648
|
+
constructor() {
|
|
649
|
+
this._pendingExecutions = new Map();
|
|
650
|
+
this._kernel = null;
|
|
651
|
+
}
|
|
652
|
+
get pendingExecutions() {
|
|
653
|
+
return this._pendingExecutions;
|
|
654
|
+
}
|
|
655
|
+
connectToKernel(kernel) {
|
|
656
|
+
this.disconnectFromKernel();
|
|
657
|
+
this._kernel = kernel;
|
|
658
|
+
kernel.iopubMessage.connect(this._onIopubMessage, this);
|
|
659
|
+
}
|
|
660
|
+
disconnectFromKernel() {
|
|
661
|
+
if (this._kernel) {
|
|
662
|
+
this._kernel.iopubMessage.disconnect(this._onIopubMessage, this);
|
|
663
|
+
this._kernel = null;
|
|
664
|
+
}
|
|
665
|
+
this._pendingExecutions.clear();
|
|
666
|
+
}
|
|
667
|
+
_onIopubMessage(_sender, msg) {
|
|
668
|
+
const parentMsgId = getParentMsgId(msg);
|
|
669
|
+
if (!parentMsgId) {
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
if (msg.header.msg_type === 'execute_input') {
|
|
673
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
674
|
+
const execCount = msg.content.execution_count;
|
|
675
|
+
this._pendingExecutions.set(parentMsgId, execCount);
|
|
676
|
+
}
|
|
677
|
+
if (msg.header.msg_type === 'status' &&
|
|
678
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
679
|
+
msg.content.execution_state === 'idle') {
|
|
680
|
+
this._pendingExecutions.delete(parentMsgId);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
685
|
+
// scanBusyCells (Step 1 of the Catch-and-Render architecture)
|
|
686
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
687
|
+
/**
|
|
688
|
+
* Scans the active notebook for cells that are currently executing (`[*]`
|
|
689
|
+
* prompt) and extracts the `msg_id` of each in-flight execution request.
|
|
690
|
+
*
|
|
691
|
+
* ### How the msg_id is obtained
|
|
692
|
+
*
|
|
693
|
+
* When JupyterLab executes a cell it calls `kernel.requestExecute()` and
|
|
694
|
+
* stores the resulting `IShellFuture` on the cell's `OutputArea`:
|
|
695
|
+
*
|
|
696
|
+
* `codeCell.outputArea.future = kernel.requestExecute({ code })`
|
|
697
|
+
*
|
|
698
|
+
* JupyterLab exposes `future` as a **write-only** setter on `OutputArea`.
|
|
699
|
+
* The underlying field is `OutputArea._future` (private). We read it here
|
|
700
|
+
* because no public getter exists, and the `msg_id` it contains is the only
|
|
701
|
+
* reliable link between the cell widget and the kernel message stream.
|
|
702
|
+
*
|
|
703
|
+
* ### Why we also need the CellExecutionTracker
|
|
704
|
+
*
|
|
705
|
+
* The `_future.msg.header.msg_id` gives us the execute-request id, but not
|
|
706
|
+
* the **execution count** the kernel assigned (the `[N]` prompt number).
|
|
707
|
+
* During execution the cell's `model.executionCount` is `null` (that is
|
|
708
|
+
* what produces the `[*]` display). The actual count was broadcast earlier
|
|
709
|
+
* in an `execute_input` iopub message which the `CellExecutionTracker`
|
|
710
|
+
* recorded. We look it up here so that the restore phase can set the
|
|
711
|
+
* correct prompt after the cell finishes.
|
|
712
|
+
*/
|
|
713
|
+
function scanBusyCells(panel, tracker) {
|
|
714
|
+
var _a, _b, _c;
|
|
715
|
+
const records = [];
|
|
716
|
+
const cells = panel.content.widgets;
|
|
717
|
+
for (let i = 0; i < cells.length; i++) {
|
|
718
|
+
const cell = cells[i];
|
|
719
|
+
if (cell.model.type !== 'code') {
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
const codeCell = cell;
|
|
723
|
+
// `executionCount === null` is the canonical indicator for a busy cell
|
|
724
|
+
// (the prompt renders `[*]`). Fresh cells also have `null`, so we
|
|
725
|
+
// additionally require an active future below.
|
|
726
|
+
if (codeCell.model.executionCount !== null) {
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
// Read the private `_future` field. This is an `IShellFuture` whose
|
|
730
|
+
// `.msg.header.msg_id` is the id the kernel uses in `parent_header`
|
|
731
|
+
// for every message belonging to this execution.
|
|
732
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
733
|
+
const future = codeCell.outputArea._future;
|
|
734
|
+
if (!((_b = (_a = future === null || future === void 0 ? void 0 : future.msg) === null || _a === void 0 ? void 0 : _a.header) === null || _b === void 0 ? void 0 : _b.msg_id)) {
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
const msgId = future.msg.header.msg_id;
|
|
738
|
+
const executionCount = (_c = tracker.pendingExecutions.get(msgId)) !== null && _c !== void 0 ? _c : null;
|
|
739
|
+
records.push({
|
|
740
|
+
cellIndex: i,
|
|
741
|
+
cellId: cell.model.id,
|
|
742
|
+
msgId,
|
|
743
|
+
executionCount
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
return records;
|
|
747
|
+
}
|
|
748
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
749
|
+
// RestoreHandler (Steps 3–6 of the Catch-and-Render architecture)
|
|
750
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
751
|
+
/**
|
|
752
|
+
* Hooks into a **restored** kernel's iopub channel and intercepts "orphaned"
|
|
753
|
+
* messages whose `parent_header.msg_id` matches a cell that was busy at
|
|
754
|
+
* checkpoint time.
|
|
755
|
+
*
|
|
756
|
+
* For each intercepted message the handler manually injects the output into
|
|
757
|
+
* the cell's `OutputArea`, replicating what JupyterLab would normally do
|
|
758
|
+
* if the original execution future were still alive.
|
|
759
|
+
*
|
|
760
|
+
* ### Output injection mechanism
|
|
761
|
+
*
|
|
762
|
+
* `codeCell.outputArea.model` implements `IOutputAreaModel`. Calling
|
|
763
|
+
* `.add(output)` with an `nbformat.IOutput`-shaped object triggers the
|
|
764
|
+
* following internal chain:
|
|
765
|
+
*
|
|
766
|
+
* 1. The model wraps the raw JSON in an `OutputModel` instance.
|
|
767
|
+
* 2. It appends the model to its internal list.
|
|
768
|
+
* – For `stream` outputs with the same `name` (stdout / stderr) as the
|
|
769
|
+
* preceding entry, the text is **merged** instead of creating a new
|
|
770
|
+
* item. This matches standard notebook behavior.
|
|
771
|
+
* 3. A Lumino `changed` signal fires.
|
|
772
|
+
* 4. The `OutputArea` widget reacts and creates the visual renderer
|
|
773
|
+
* (e.g. `<pre>` for streams, a MIME renderer for display_data).
|
|
774
|
+
*
|
|
775
|
+
* Because all of this happens through the model's public API the rendered
|
|
776
|
+
* output is indistinguishable from one produced by the normal future-based
|
|
777
|
+
* routing.
|
|
778
|
+
*/
|
|
779
|
+
class RestoreHandler {
|
|
780
|
+
constructor() {
|
|
781
|
+
this._activeMsgIds = new Map();
|
|
782
|
+
this._panel = null;
|
|
783
|
+
this._kernel = null;
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Begin intercepting orphaned messages for the given busy cells.
|
|
787
|
+
* Call immediately after `sessionContext.changeKernel()` resolves.
|
|
788
|
+
*/
|
|
789
|
+
hookRestoredKernel(panel, busyCells) {
|
|
790
|
+
var _a;
|
|
791
|
+
if (busyCells.length === 0) {
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
this.dispose();
|
|
795
|
+
this._panel = panel;
|
|
796
|
+
for (const record of busyCells) {
|
|
797
|
+
this._activeMsgIds.set(record.msgId, record);
|
|
798
|
+
}
|
|
799
|
+
// Re-apply the busy indicator ([*]) so the user sees the cells as
|
|
800
|
+
// still running while output continues to stream in.
|
|
801
|
+
for (const record of busyCells) {
|
|
802
|
+
const cell = this._findCell(panel, record);
|
|
803
|
+
if (cell) {
|
|
804
|
+
cell.model.executionCount = null;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
const kernel = (_a = panel.sessionContext.session) === null || _a === void 0 ? void 0 : _a.kernel;
|
|
808
|
+
if (!kernel) {
|
|
809
|
+
console.warn('[RestoreHandler] No kernel on panel after changeKernel(); ' +
|
|
810
|
+
'cannot hook iopub. Orphaned outputs will be lost.');
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
this._kernel = kernel;
|
|
814
|
+
kernel.iopubMessage.connect(this._onIopubMessage, this);
|
|
815
|
+
console.log(`[RestoreHandler] Hooked into restored kernel iopub. ` +
|
|
816
|
+
`Tracking ${busyCells.length} busy cell(s):`, busyCells.map(r => `cell[${r.cellIndex}] (${r.cellId}) → ${r.msgId}`));
|
|
817
|
+
}
|
|
818
|
+
dispose() {
|
|
819
|
+
if (this._kernel) {
|
|
820
|
+
this._kernel.iopubMessage.disconnect(this._onIopubMessage, this);
|
|
821
|
+
this._kernel = null;
|
|
822
|
+
}
|
|
823
|
+
this._activeMsgIds.clear();
|
|
824
|
+
this._panel = null;
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Locate the CodeCell for a given record. We first try the saved index
|
|
828
|
+
* (fast path) and verify by cell ID, then fall back to a linear scan.
|
|
829
|
+
* This handles the case where the user inserted or deleted cells between
|
|
830
|
+
* checkpoint and restore.
|
|
831
|
+
*/
|
|
832
|
+
_findCell(panel, record) {
|
|
833
|
+
const cells = panel.content.widgets;
|
|
834
|
+
if (record.cellIndex < cells.length) {
|
|
835
|
+
const candidate = cells[record.cellIndex];
|
|
836
|
+
if (candidate.model.type === 'code' &&
|
|
837
|
+
candidate.model.id === record.cellId) {
|
|
838
|
+
return candidate;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
for (let i = 0; i < cells.length; i++) {
|
|
842
|
+
const c = cells[i];
|
|
843
|
+
if (c.model.id === record.cellId && c.model.type === 'code') {
|
|
844
|
+
return c;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
// ── iopub signal handler ──────────────────────────────────────────────
|
|
850
|
+
_onIopubMessage(_sender, msg) {
|
|
851
|
+
const parentMsgId = getParentMsgId(msg);
|
|
852
|
+
if (!parentMsgId) {
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
const record = this._activeMsgIds.get(parentMsgId);
|
|
856
|
+
if (!record) {
|
|
857
|
+
// Not one of our tracked orphaned executions — let JupyterLab's
|
|
858
|
+
// normal future-based routing handle this message.
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
if (!this._panel) {
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
const codeCell = this._findCell(this._panel, record);
|
|
865
|
+
if (!codeCell) {
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
const msgType = msg.header.msg_type;
|
|
869
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
870
|
+
const content = msg.content;
|
|
871
|
+
// ── Step 5: Inject output into the cell's OutputArea ────────────
|
|
872
|
+
//
|
|
873
|
+
// We construct an nbformat-compatible output object and pass it to
|
|
874
|
+
// `outputArea.model.add()`. The model validates the shape, wraps it
|
|
875
|
+
// in an OutputModel, and emits a `changed` signal that causes the
|
|
876
|
+
// OutputArea widget to render the new entry.
|
|
877
|
+
switch (msgType) {
|
|
878
|
+
case 'stream':
|
|
879
|
+
codeCell.outputArea.model.add({
|
|
880
|
+
output_type: 'stream',
|
|
881
|
+
name: content.name,
|
|
882
|
+
text: content.text
|
|
883
|
+
});
|
|
884
|
+
break;
|
|
885
|
+
case 'error':
|
|
886
|
+
codeCell.outputArea.model.add({
|
|
887
|
+
output_type: 'error',
|
|
888
|
+
ename: content.ename,
|
|
889
|
+
evalue: content.evalue,
|
|
890
|
+
traceback: content.traceback
|
|
891
|
+
});
|
|
892
|
+
break;
|
|
893
|
+
case 'execute_result':
|
|
894
|
+
codeCell.outputArea.model.add({
|
|
895
|
+
output_type: 'execute_result',
|
|
896
|
+
data: content.data,
|
|
897
|
+
metadata: content.metadata,
|
|
898
|
+
execution_count: content.execution_count
|
|
899
|
+
});
|
|
900
|
+
break;
|
|
901
|
+
case 'display_data':
|
|
902
|
+
codeCell.outputArea.model.add({
|
|
903
|
+
output_type: 'display_data',
|
|
904
|
+
data: content.data,
|
|
905
|
+
metadata: content.metadata
|
|
906
|
+
});
|
|
907
|
+
break;
|
|
908
|
+
// ── Step 6: Lifecycle Cleanup ─────────────────────────────────
|
|
909
|
+
//
|
|
910
|
+
// A `status: idle` message with our tracked msg_id means the
|
|
911
|
+
// kernel finished executing this cell. We:
|
|
912
|
+
// a) Set the cell's execution count → prompt changes [*] → [N]
|
|
913
|
+
// b) Remove the msg_id from the tracking map
|
|
914
|
+
// c) Disconnect from iopub when no busy cells remain
|
|
915
|
+
case 'status':
|
|
916
|
+
if (content.execution_state === 'idle') {
|
|
917
|
+
if (record.executionCount !== null) {
|
|
918
|
+
codeCell.model.executionCount = record.executionCount;
|
|
919
|
+
}
|
|
920
|
+
this._activeMsgIds.delete(parentMsgId);
|
|
921
|
+
console.log(`[RestoreHandler] Cell[${record.cellIndex}] execution ` +
|
|
922
|
+
`complete (exec count: ${record.executionCount}). ` +
|
|
923
|
+
`${this._activeMsgIds.size} cell(s) still pending.`);
|
|
924
|
+
if (this._activeMsgIds.size === 0) {
|
|
925
|
+
console.log('[RestoreHandler] All restored cells complete. ' +
|
|
926
|
+
'Disconnecting iopub hook.');
|
|
927
|
+
this.dispose();
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
break;
|
|
931
|
+
default:
|
|
932
|
+
break;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
|
|
518
938
|
/***/ }
|
|
519
939
|
|
|
520
940
|
}]);
|
|
521
|
-
//# sourceMappingURL=lib_index_js.
|
|
941
|
+
//# sourceMappingURL=lib_index_js.6dc5fa8b4a9bc62a809d.js.map
|