@fails-components/jupyter-launcher 0.0.1-alpha.10

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.
package/src/index.ts ADDED
@@ -0,0 +1,936 @@
1
+ import {
2
+ ILabStatus,
3
+ JupyterFrontEnd,
4
+ JupyterFrontEndPlugin
5
+ } from '@jupyterlab/application';
6
+ import { IDocumentManager } from '@jupyterlab/docmanager';
7
+ import { ServerConnection, Kernel } from '@jupyterlab/services';
8
+ import { PageConfig, URLExt } from '@jupyterlab/coreutils';
9
+ import { ISessionContext } from '@jupyterlab/apputils';
10
+ import { IDocumentWidget } from '@jupyterlab/docregistry';
11
+ import { INotebookShell } from '@jupyter-notebook/application';
12
+ import { NotebookActions, NotebookPanel } from '@jupyterlab/notebook';
13
+ import { JSONObject, PartialJSONObject, Token } from '@lumino/coreutils';
14
+ import { ISignal, Signal } from '@lumino/signaling';
15
+ import { Panel } from '@lumino/widgets';
16
+
17
+ export interface IScreenShotOpts {
18
+ dpi: number;
19
+ }
20
+
21
+ export interface IAppletScreenshottaker {
22
+ takeAppScreenshot: (opts: IScreenShotOpts) => Promise<Blob | undefined>;
23
+ }
24
+
25
+ interface IContentEvent {
26
+ task: string;
27
+ }
28
+
29
+ export interface ILoadJupyterContentEvent extends IContentEvent {
30
+ task: 'loadFile';
31
+ fileData: object | undefined;
32
+ fileName: string;
33
+ }
34
+
35
+ export interface ISavedJupyterContentEvent extends IContentEvent {
36
+ task: 'savedFile';
37
+ fileName: string;
38
+ }
39
+
40
+ export type IContentEventType =
41
+ | ILoadJupyterContentEvent
42
+ | ISavedJupyterContentEvent; // use union
43
+
44
+ export interface IFailsCallbacks {
45
+ callContents?: (event: IContentEventType) => Promise<any>;
46
+ postMessageToFails?: (message: any, transfer?: Transferable[]) => void;
47
+ }
48
+
49
+ export const IFailsLauncherInfo = new Token<IFailsLauncherInfo>(
50
+ '@fails-components/jupyter-fails:IFailsLauncherInfo',
51
+ 'A service to communicate with FAILS.'
52
+ );
53
+
54
+ export interface IFailsLauncherInit {
55
+ inLecture: boolean;
56
+ selectedAppid: string | undefined;
57
+ reportMetadata?: (metadata: PartialJSONObject) => void;
58
+ }
59
+
60
+ export interface IFailsAppletSize {
61
+ appid: string;
62
+ width: number;
63
+ height: number;
64
+ }
65
+
66
+ export interface IAppletWidgetRegistry {}
67
+
68
+ export interface IFailsInterceptorUpdateMessage {
69
+ path: string;
70
+ mime: string;
71
+ state: JSONObject;
72
+ }
73
+
74
+ export interface IFailsLauncherInfo extends IFailsLauncherInit {
75
+ inLectureChanged: ISignal<IFailsLauncherInfo, boolean>;
76
+ selectedAppidChanged: ISignal<this, string | undefined>;
77
+ appletSizes: { [key: string]: IFailsAppletSize };
78
+ appletSizesChanged: ISignal<this, { [key: string]: IFailsAppletSize }>;
79
+ updateMessageArrived?: ISignal<
80
+ IAppletWidgetRegistry,
81
+ IFailsInterceptorUpdateMessage
82
+ >;
83
+ remoteUpdateMessageArrived: ISignal<
84
+ IFailsLauncherInfo,
85
+ IFailsInterceptorUpdateMessage
86
+ >;
87
+ }
88
+
89
+ export interface IReplyJupyter {
90
+ requestId?: number;
91
+ }
92
+
93
+ export interface IGDPRProxyInfo {
94
+ allowedSites?: string[] | undefined;
95
+ proxySites?: string[] | undefined;
96
+ proxyURL: string;
97
+ }
98
+
99
+ export interface ILoadJupyterInfo {
100
+ type: 'loadJupyter';
101
+ inLecture: boolean;
102
+ rerunAtStartup: boolean;
103
+ installScreenShotPatches: boolean;
104
+ installGDPRProxy?: IGDPRProxyInfo;
105
+ appid?: string;
106
+ fileName: string;
107
+ fileData: object | undefined; // TODO replace object with meaning full type
108
+ kernelName: 'python' | 'xpython' | undefined;
109
+ }
110
+
111
+ export interface ISaveJupyter {
112
+ type: 'saveJupyter';
113
+ fileName: string;
114
+ }
115
+
116
+ export interface IActivateApp {
117
+ type: 'activateApp';
118
+ inLecture: boolean;
119
+ appid?: string;
120
+ }
121
+
122
+ export interface IScreenshotApp extends IScreenShotOpts {
123
+ type: 'screenshotApp';
124
+ }
125
+
126
+ export interface IActivateInterceptor {
127
+ type: 'activateInterceptor';
128
+ activate: boolean;
129
+ }
130
+
131
+ export interface IGetLicenses {
132
+ type: 'getLicenses';
133
+ }
134
+
135
+ export interface IRestartKernelAndRerunCells {
136
+ type: 'restartKernelAndRerunCells';
137
+ }
138
+
139
+ export interface IInterceptorUpdate {
140
+ path: string;
141
+ mime: string;
142
+ state: JSONObject;
143
+ }
144
+
145
+ export interface IReceiveInterceptorUpdate extends IInterceptorUpdate {
146
+ type: 'receiveInterceptorUpdate';
147
+ }
148
+
149
+ export type IFailsToJupyterMessage =
150
+ | IActivateApp
151
+ | IActivateInterceptor
152
+ | IGetLicenses
153
+ | IReceiveInterceptorUpdate
154
+ | IRestartKernelAndRerunCells
155
+ | ILoadJupyterInfo
156
+ | IScreenshotApp
157
+ | ISaveJupyter;
158
+
159
+ export interface IAppLoaded {
160
+ task: 'appLoaded';
161
+ }
162
+
163
+ export interface IDocDirty {
164
+ task: 'docDirty';
165
+ dirty: boolean;
166
+ }
167
+
168
+ export interface IReportMetadata {
169
+ task: 'reportMetadata';
170
+ metadata: {
171
+ failsApp?: JSONObject;
172
+ kernelspec?: JSONObject;
173
+ };
174
+ }
175
+
176
+ export interface IReportFailsAppletSizes {
177
+ task: 'reportFailsAppletSizes';
178
+ appletSizes: { [key: string]: IFailsAppletSize };
179
+ }
180
+
181
+ export interface IReportKernelStatus {
182
+ task: 'reportKernelStatus';
183
+ status: string;
184
+ }
185
+
186
+ export interface ISendInterceptorUpdate extends IInterceptorUpdate {
187
+ task: 'sendInterceptorUpdate';
188
+ }
189
+
190
+ export type IJupyterToFailsMessage =
191
+ | IReportMetadata
192
+ | IReportFailsAppletSizes
193
+ | ISendInterceptorUpdate
194
+ | IReportKernelStatus
195
+ | IAppLoaded
196
+ | IDocDirty;
197
+
198
+ let screenShotPatchInstalled = false;
199
+ const installScreenShotPatches = () => {
200
+ if (screenShotPatchInstalled) {
201
+ return;
202
+ }
203
+
204
+ // Monkey patch the HTMLCanvasObject
205
+ const oldGetContext = HTMLCanvasElement.prototype.getContext;
206
+ // @ts-expect-error type do not matter
207
+ HTMLCanvasElement.prototype.getContext = function (
208
+ contexttype: string,
209
+ contextAttributes?:
210
+ | CanvasRenderingContext2DSettings
211
+ | WebGLContextAttributes
212
+ ) {
213
+ if (contexttype === 'webgl' || contexttype === 'webgl2') {
214
+ const newcontext = { ...contextAttributes } as any;
215
+ newcontext.preserveDrawingBuffer = true;
216
+ return oldGetContext.apply(this, [contexttype, newcontext]);
217
+ }
218
+ return oldGetContext.apply(this, [contexttype, contextAttributes]);
219
+ };
220
+ screenShotPatchInstalled = true;
221
+ };
222
+
223
+ // Monkey patch fetch, e.g. for GDPR compliance
224
+ let fetchPatchInstalled = false;
225
+ const installFetchPatches = ({
226
+ allowedSites = undefined,
227
+ proxySites = undefined,
228
+ proxyURL
229
+ }: IGDPRProxyInfo) => {
230
+ if (fetchPatchInstalled) {
231
+ return;
232
+ }
233
+ const allowedOrigins = [location.origin, ...(allowedSites || [])];
234
+ const allowedOriginsString = allowedOrigins
235
+ .map(el => "'" + new URL(el).origin + "'")
236
+ .join(',');
237
+ const proxySitesString = (proxySites || [])
238
+ .map(el => "'" + new URL(el).origin + "'")
239
+ .join(',');
240
+
241
+ // Monkey patch fetch
242
+ const oldFetch = globalThis.fetch;
243
+ globalThis.fetch = async (
244
+ url: RequestInfo | URL,
245
+ options: RequestInit = {}
246
+ ) => {
247
+ const urlObj =
248
+ url instanceof URL
249
+ ? url
250
+ : new URL(url instanceof Request ? url.url : url, location.href);
251
+ if (allowedOrigins.includes(urlObj.origin)) {
252
+ return oldFetch(url instanceof Request ? url : urlObj, options);
253
+ }
254
+ if (proxySites && proxySites.includes(urlObj.origin)) {
255
+ // rewrite the URL and response
256
+ const resURL = proxyURL + urlObj.hostname + urlObj.pathname;
257
+ if (url instanceof Request) {
258
+ const request = url;
259
+ const newRequest = new Request(resURL, {
260
+ method: request.method,
261
+ headers: request.headers,
262
+ body: request.body,
263
+ credentials: request.credentials,
264
+ mode: request.mode,
265
+ integrity: request.integrity,
266
+ keepalive: request.keepalive,
267
+ referrerPolicy: request.referrerPolicy,
268
+ cache: request.cache,
269
+ redirect: request.redirect,
270
+ referrer: request.referrer,
271
+ signal: request.signal
272
+ });
273
+ return oldFetch(newRequest, options);
274
+ } else {
275
+ urlObj.href = resURL;
276
+ return oldFetch(urlObj, options);
277
+ }
278
+ }
279
+ console.log('alien fetch URL:', urlObj.href);
280
+ return new Response('Blocked domain, access forbidden', {
281
+ status: 403,
282
+ statusText: 'Forbidden',
283
+ headers: { 'Content-Type': 'text/plain' }
284
+ });
285
+ };
286
+ const oldImportScripts = globalThis.importScripts;
287
+ globalThis.importScripts = (...args) => {
288
+ const newargs = args.map(url => {
289
+ const urlObj = new URL(url, location.href);
290
+ if (allowedOrigins.includes(urlObj.origin)) {
291
+ return url;
292
+ }
293
+ if (proxySites && proxySites.includes(urlObj.origin)) {
294
+ return proxyURL + urlObj.hostname + urlObj.pathname;
295
+ }
296
+ throw new Error('Script is from blocked URL');
297
+ });
298
+ return oldImportScripts(...newargs);
299
+ };
300
+ const oldWorker = Worker;
301
+ const NewWorker = function (
302
+ script: string | URL,
303
+ options?: WorkerOptions | undefined
304
+ ) {
305
+ const scriptURL =
306
+ script instanceof URL ? script : new URL(script, location.href);
307
+ if (!allowedOrigins.includes(scriptURL.origin)) {
308
+ console.log('Creating worker from blocked origin:', scriptURL.origin);
309
+ return;
310
+ }
311
+ console.log('Tap into creating worker:', scriptURL.href);
312
+ const injectPrefix = `(function() {
313
+ const allowedOrigins = [ ${allowedOriginsString} ];
314
+ const proxySites = [ ${proxySitesString} ];
315
+ const proxyURL = '${proxyURL}';
316
+ const oldFetch = globalThis.fetch;
317
+ globalThis.fetch = async function(url, options = {}) {
318
+ const urlObj = url instanceof URL ? url : new URL(url instanceof Request ? url.url : url, location.href);
319
+ if (allowedOrigins.includes(urlObj.origin)) {
320
+ return oldFetch(url instanceof Request ? url : urlObj, options);
321
+ }
322
+ if (proxySites && proxySites.includes(urlObj.origin)) {
323
+ // rewrite the URL and response
324
+ const resURL = proxyURL + urlObj.hostname + urlObj.pathname;
325
+ console.log('proxy url debug', resURL, urlObj.href);
326
+ if (url instanceof Request) {
327
+ const request = url;
328
+ const newRequest = new Request(resURL, {
329
+ method: request.method,
330
+ headers: request.headers,
331
+ body: request.body,
332
+ credentials: request.credentials,
333
+ mode: request.mode,
334
+ integrity: request.integrity,
335
+ keepalive: request.keepalive,
336
+ referrerPolicy: request.referrerPolicy,
337
+ cache: request.cache,
338
+ redirect: request.redirect,
339
+ referrer: request.referrer,
340
+ signal: request.signal
341
+ });
342
+ console.log('proxy request debug', newRequest, request);
343
+ return oldFetch(newRequest, options);
344
+ } else {
345
+ urlObj.href = resURL;
346
+ return oldFetch(urlObj, options);
347
+ }
348
+ }
349
+ console.log('alien fetch URL worker:', urlObj.href);
350
+ return new Response('Blocked domain, access forbidden',{
351
+ status: 403,
352
+ statusText: 'Forbidden',
353
+ headers: { 'Content-Type': 'text/plain' }
354
+ });
355
+ };
356
+ const oldImportScripts = globalThis.importScripts;
357
+ globalThis.importScripts = (...args) => {
358
+ const newargs = args.map(url => {
359
+ const urlObj = new URL(url, location.href);
360
+ if (allowedOrigins.includes(urlObj.origin)) {
361
+ return url;
362
+ }
363
+ if (proxySites && proxySites.includes(urlObj.origin)) {
364
+ return proxyURL + urlObj.hostname + urlObj.pathname;
365
+ }
366
+ throw new Error('Script is from blocked URL');
367
+ });
368
+ return oldImportScripts(...newargs);
369
+ };
370
+ Object.defineProperty(globalThis, 'location', {
371
+ value: new URL('${scriptURL.href}'),
372
+ writable: false,
373
+ configurable: false,
374
+ enumerable: true,
375
+ });
376
+ })();`;
377
+ const inject =
378
+ injectPrefix +
379
+ (options?.type === 'module'
380
+ ? `
381
+ import('${scriptURL.href}').catch(error => {
382
+ console.error('Can not load module patching Worker:', error);
383
+ });`
384
+ : `
385
+ importScripts('${scriptURL.href}')
386
+ `);
387
+ const blob = new Blob([inject], { type: 'application/javascript' });
388
+ const url = URL.createObjectURL(blob);
389
+ const newWorker = new oldWorker(url, options);
390
+ newWorker.addEventListener(
391
+ 'message',
392
+ () => {
393
+ URL.revokeObjectURL(url);
394
+ },
395
+ { once: true }
396
+ );
397
+ return newWorker;
398
+ } as unknown as typeof Worker;
399
+
400
+ NewWorker.prototype = oldWorker.prototype;
401
+ NewWorker.prototype.constructor = NewWorker;
402
+ globalThis.Worker = NewWorker;
403
+ fetchPatchInstalled = true;
404
+ };
405
+
406
+ class FailsLauncherInfo implements IFailsLauncherInfo {
407
+ constructor(options?: IFailsLauncherInfo) {
408
+ this._inLecture = options?.inLecture ?? false;
409
+ this._selectedAppid = options?.selectedAppid ?? undefined;
410
+ }
411
+
412
+ get inLectureChanged(): ISignal<this, boolean> {
413
+ return this._inLectureChanged;
414
+ }
415
+
416
+ get inLecture(): boolean {
417
+ return this._inLecture;
418
+ }
419
+
420
+ set inLecture(value: boolean) {
421
+ if (value === this._inLecture) {
422
+ return;
423
+ }
424
+ this._inLecture = value;
425
+ if (value === false && this._selectedAppid) {
426
+ this._selectedAppid = undefined;
427
+ this._selectedAppidChanged.emit(undefined);
428
+ }
429
+ this._inLectureChanged.emit(value);
430
+ }
431
+
432
+ get selectedAppid(): string | undefined {
433
+ return this._selectedAppid;
434
+ }
435
+
436
+ get selectedAppidChanged(): ISignal<this, string | undefined> {
437
+ return this._selectedAppidChanged;
438
+ }
439
+
440
+ set selectedAppid(appid: string | undefined) {
441
+ if (appid === this._selectedAppid) {
442
+ return;
443
+ }
444
+ this._selectedAppid = appid;
445
+ if (!this._inLecture && typeof appid !== 'undefined') {
446
+ this._inLecture = true;
447
+ this._inLectureChanged.emit(true);
448
+ } else if (this._inLecture && typeof appid === 'undefined') {
449
+ this._inLecture = false;
450
+ this._inLectureChanged.emit(false);
451
+ }
452
+ this._selectedAppidChanged.emit(appid);
453
+ }
454
+
455
+ get appletSizes() {
456
+ return this._appletSizesProxy;
457
+ }
458
+
459
+ get appletSizesChanged() {
460
+ return this._appletSizesChanged;
461
+ }
462
+
463
+ get updateMessageArrived():
464
+ | ISignal<IAppletWidgetRegistry, IFailsInterceptorUpdateMessage>
465
+ | undefined {
466
+ return this._updateMessageArrived;
467
+ }
468
+
469
+ set updateMessageArrived(
470
+ updateMessageArrived:
471
+ | ISignal<IAppletWidgetRegistry, IFailsInterceptorUpdateMessage>
472
+ | undefined
473
+ ) {
474
+ this._updateMessageArrived = updateMessageArrived;
475
+ }
476
+
477
+ get remoteUpdateMessageArrived(): ISignal<
478
+ IFailsLauncherInfo,
479
+ IFailsInterceptorUpdateMessage
480
+ > {
481
+ return this._remoteUpdateMessageArrived;
482
+ }
483
+
484
+ receiveRemoteUpdateMessage(message: IFailsInterceptorUpdateMessage) {
485
+ this._remoteUpdateMessageArrived.emit(message);
486
+ }
487
+
488
+ private _inLecture: boolean;
489
+ private _inLectureChanged = new Signal<this, boolean>(this);
490
+ private _selectedAppid: string | undefined;
491
+ private _selectedAppidChanged = new Signal<this, string | undefined>(this);
492
+ private _updateMessageArrived:
493
+ | ISignal<IAppletWidgetRegistry, IFailsInterceptorUpdateMessage>
494
+ | undefined;
495
+ private _remoteUpdateMessageArrived: Signal<
496
+ IFailsLauncherInfo,
497
+ IFailsInterceptorUpdateMessage
498
+ > = new Signal<this, IFailsInterceptorUpdateMessage>(this);
499
+ private _appletSizes: { [key: string]: IFailsAppletSize } = {};
500
+ private _appletSizesChanged = new Signal<
501
+ this,
502
+ { [key: string]: IFailsAppletSize }
503
+ >(this);
504
+ private _appletSizesProxy = new Proxy(this._appletSizes, {
505
+ get: (target, property) => {
506
+ if (typeof property !== 'symbol') {
507
+ return target[property];
508
+ }
509
+ },
510
+ set: (target, property, value) => {
511
+ if (typeof property !== 'symbol') {
512
+ if (target[property] !== value) {
513
+ target[property] = value;
514
+ this._appletSizesChanged.emit(target);
515
+ }
516
+ return true;
517
+ }
518
+ return false;
519
+ }
520
+ });
521
+ }
522
+
523
+ function activateFailsLauncher(
524
+ app: JupyterFrontEnd,
525
+ docManager: IDocumentManager,
526
+ status: ILabStatus,
527
+ shell: INotebookShell | null
528
+ ): IFailsLauncherInfo {
529
+ // parts taken from repl-extension
530
+ const { /* commands, */ serviceManager, started } = app;
531
+ Promise.all([started, serviceManager.ready]).then(async () => {
532
+ /* commands.execute('notebook:create-new', {
533
+ kernelId: undefined,
534
+ kernelName: undefined
535
+ }); */
536
+ // TODO select kernel and replace with content
537
+ });
538
+ // TODO steer with messages
539
+ const { docRegistry } = app;
540
+ const failsLauncherInfo: IFailsLauncherInfo = new FailsLauncherInfo();
541
+ let currentDocWidget: IDocumentWidget | undefined;
542
+
543
+ const serverSettings = app.serviceManager.serverSettings;
544
+ const licensesUrl =
545
+ URLExt.join(PageConfig.getBaseUrl(), PageConfig.getOption('licensesUrl')) +
546
+ '/';
547
+
548
+ // Install Messagehandler
549
+ if (!(window as any).failsCallbacks) {
550
+ (window as any).failsCallbacks = {};
551
+ }
552
+ let senderOrigin: string | undefined;
553
+ const _failsCallbacks = (window as any).failsCallbacks as IFailsCallbacks;
554
+ _failsCallbacks.postMessageToFails = (
555
+ message: any,
556
+ transfer?: Transferable[]
557
+ ) => {
558
+ if (typeof senderOrigin !== 'undefined') {
559
+ window.parent.postMessage(message, senderOrigin, transfer);
560
+ }
561
+ };
562
+ status.dirtySignal.connect((sender, dirty) => {
563
+ _failsCallbacks.postMessageToFails!({
564
+ task: 'docDirty',
565
+ dirty
566
+ });
567
+ });
568
+ failsLauncherInfo.reportMetadata = metadata => {
569
+ _failsCallbacks.postMessageToFails!({
570
+ task: 'reportMetadata',
571
+ metadata
572
+ });
573
+ };
574
+ failsLauncherInfo.inLectureChanged.connect(
575
+ (sender: IFailsLauncherInfo, inLecture) => {
576
+ if (shell !== null) {
577
+ shell.menu.setHidden(inLecture);
578
+ }
579
+ }
580
+ );
581
+ failsLauncherInfo.appletSizesChanged.connect(
582
+ (
583
+ sender: IFailsLauncherInfo,
584
+ appletSizes: { [key: string]: IFailsAppletSize }
585
+ ) => {
586
+ _failsCallbacks.postMessageToFails!({
587
+ task: 'reportFailsAppletSizes',
588
+ appletSizes
589
+ });
590
+ }
591
+ );
592
+ let interceptorActivated = false;
593
+ const sendInterceptorUpdate = (
594
+ sender: IAppletWidgetRegistry,
595
+ message: IFailsInterceptorUpdateMessage
596
+ ): void => {
597
+ _failsCallbacks.postMessageToFails!({
598
+ task: 'sendInterceptorUpdate',
599
+ ...message
600
+ });
601
+ };
602
+
603
+ window.addEventListener('message', (event: MessageEvent<any>) => {
604
+ // TODO identify the embedding page.
605
+ if (typeof senderOrigin === 'undefined') {
606
+ senderOrigin = event.origin;
607
+ }
608
+ // handle FAILS control messages
609
+ switch (event.data.type) {
610
+ case 'loadJupyter':
611
+ {
612
+ const loadJupyterInfo = event.data as ILoadJupyterInfo;
613
+ failsLauncherInfo.inLecture =
614
+ loadJupyterInfo.inLecture ?? failsLauncherInfo.inLecture;
615
+ docManager.autosave = false; // do not autosave
616
+ if (loadJupyterInfo.installScreenShotPatches) {
617
+ installScreenShotPatches();
618
+ }
619
+ if (loadJupyterInfo.installGDPRProxy) {
620
+ installFetchPatches(loadJupyterInfo.installGDPRProxy);
621
+ }
622
+ // TODO send fileData to contents together with filename, and wait for fullfillment
623
+ // may be use a promise for fullfillment, e.g. pass a resolve
624
+ // afterwards we load the file or new file into to the contexts
625
+ // we may also send license information
626
+ _failsCallbacks.callContents!({
627
+ task: 'loadFile',
628
+ fileData: loadJupyterInfo.fileData,
629
+ fileName: loadJupyterInfo.fileName
630
+ })
631
+ .then(() => {
632
+ // ok the file is placed inside the file system now load it into the app
633
+ const kernel: Partial<Kernel.IModel> = {
634
+ name: loadJupyterInfo.kernelName || 'python' // 'xpython' for xeus
635
+ };
636
+ const defaultFactory = docRegistry.defaultWidgetFactory(
637
+ loadJupyterInfo.fileName
638
+ ).name;
639
+ const factory = defaultFactory;
640
+ currentDocWidget = docManager.open(
641
+ loadJupyterInfo.fileName,
642
+ factory,
643
+ kernel,
644
+ {
645
+ ref: '_noref'
646
+ }
647
+ );
648
+ if (loadJupyterInfo.appid) {
649
+ failsLauncherInfo.selectedAppid = loadJupyterInfo.appid;
650
+ }
651
+ let rerunAfterKernelStart = loadJupyterInfo.rerunAtStartup;
652
+ if (typeof currentDocWidget !== 'undefined') {
653
+ const notebookPanel = currentDocWidget as NotebookPanel;
654
+ notebookPanel.sessionContext.statusChanged.connect(
655
+ (context: ISessionContext, status: Kernel.Status) => {
656
+ _failsCallbacks.postMessageToFails!({
657
+ task: 'reportKernelStatus',
658
+ status
659
+ });
660
+ if (status === 'idle' && rerunAfterKernelStart) {
661
+ console.log('Run all cells after startup');
662
+ const { context, content } = notebookPanel;
663
+ const cells = content.widgets;
664
+ NotebookActions.runCells(
665
+ content,
666
+ cells,
667
+ context.sessionContext
668
+ )
669
+ .then(() => {
670
+ console.log('Run all cells after startup finished');
671
+ })
672
+ .catch(error => {
673
+ console.log(
674
+ 'Run all cells after startup error',
675
+ error
676
+ );
677
+ });
678
+
679
+ rerunAfterKernelStart = false;
680
+ }
681
+ }
682
+ );
683
+ }
684
+ })
685
+ .catch(error => {
686
+ console.log('Problem task load file', error);
687
+ });
688
+ }
689
+ break;
690
+ case 'saveJupyter':
691
+ {
692
+ const saveJupyter = event.data as ISaveJupyter;
693
+ if (typeof currentDocWidget === 'undefined') {
694
+ _failsCallbacks.postMessageToFails!({
695
+ requestId: event.data.requestId,
696
+ task: 'saveJupyter',
697
+ error: 'No document loaded'
698
+ });
699
+ break;
700
+ }
701
+ const context = docManager.contextForWidget(currentDocWidget);
702
+ if (typeof context === 'undefined') {
703
+ _failsCallbacks.postMessageToFails!({
704
+ requestId: event.data.requestId,
705
+ task: 'saveJupyter',
706
+ error: 'No document context'
707
+ });
708
+ break;
709
+ }
710
+ context
711
+ .save()
712
+ .then(() => {
713
+ // ok it was save to our virtual disk
714
+ return _failsCallbacks.callContents!({
715
+ task: 'savedFile',
716
+ fileName: saveJupyter.fileName
717
+ });
718
+ })
719
+ .then(({ fileData }) => {
720
+ _failsCallbacks.postMessageToFails!({
721
+ requestId: event.data.requestId,
722
+ task: 'saveJupyter',
723
+ fileData
724
+ });
725
+ })
726
+ .catch((error: Error) => {
727
+ _failsCallbacks.postMessageToFails!({
728
+ requestId: event.data.requestId,
729
+ task: 'saveJupyter',
730
+ error: error.toString()
731
+ });
732
+ });
733
+ }
734
+ break;
735
+ case 'activateApp':
736
+ {
737
+ const activateApp = event.data as IActivateApp;
738
+ if (activateApp.inLecture) {
739
+ failsLauncherInfo.selectedAppid = activateApp.appid;
740
+ } else {
741
+ failsLauncherInfo.inLecture = false;
742
+ }
743
+ _failsCallbacks.postMessageToFails!({
744
+ requestId: event.data.requestId,
745
+ task: 'activateApp'
746
+ });
747
+ }
748
+ break;
749
+ case 'screenshotApp':
750
+ {
751
+ const screenshotApp = event.data as IScreenshotApp;
752
+ const notebookPanel = currentDocWidget as NotebookPanel;
753
+ if (
754
+ !(typeof (notebookPanel as any)['takeAppScreenshot'] === 'function')
755
+ ) {
756
+ _failsCallbacks.postMessageToFails!({
757
+ requestId: event.data.requestId,
758
+ task: 'screenshotApp',
759
+ error: 'Take App Screenshot unsupported'
760
+ });
761
+ }
762
+ const screenShotTaker =
763
+ notebookPanel as any as IAppletScreenshottaker;
764
+ screenShotTaker
765
+ .takeAppScreenshot({ dpi: screenshotApp.dpi })
766
+ .then(async screenshot => {
767
+ if (screenshot) {
768
+ const data = await screenshot.arrayBuffer();
769
+ _failsCallbacks.postMessageToFails?.(
770
+ {
771
+ requestId: event.data.requestId,
772
+ task: 'screenshotApp',
773
+ screenshot: { data, type: screenshot.type }
774
+ },
775
+ [data]
776
+ ); // TODO add transferable
777
+ } else {
778
+ _failsCallbacks.postMessageToFails?.({
779
+ requestId: event.data.requestId,
780
+ task: 'screenshotApp',
781
+ error: 'Screenshot failed?'
782
+ });
783
+ }
784
+ })
785
+ .catch((error: Error) => {
786
+ console.log('Screenshot error', error);
787
+ _failsCallbacks.postMessageToFails!({
788
+ requestId: event.data.requestId,
789
+ task: 'screenshotApp',
790
+ error: error.toString()
791
+ });
792
+ });
793
+ }
794
+ break;
795
+ case 'activateInterceptor':
796
+ {
797
+ const activateInterceptor = event.data as IActivateInterceptor;
798
+ if (
799
+ interceptorActivated !== activateInterceptor.activate &&
800
+ failsLauncherInfo.updateMessageArrived
801
+ ) {
802
+ if (!interceptorActivated) {
803
+ failsLauncherInfo.updateMessageArrived.connect(
804
+ sendInterceptorUpdate
805
+ );
806
+ interceptorActivated = true;
807
+ } else {
808
+ failsLauncherInfo.updateMessageArrived.disconnect(
809
+ sendInterceptorUpdate
810
+ );
811
+ interceptorActivated = false;
812
+ }
813
+ }
814
+ _failsCallbacks.postMessageToFails!({
815
+ requestId: event.data.requestId,
816
+ task: 'activateInterceptor'
817
+ });
818
+ }
819
+ break;
820
+ case 'receiveInterceptorUpdate':
821
+ {
822
+ const receiveInterceptorUpdate =
823
+ event.data as IReceiveInterceptorUpdate;
824
+ const { path, mime, state } = receiveInterceptorUpdate;
825
+ const launcherInfo = failsLauncherInfo as FailsLauncherInfo;
826
+ launcherInfo.receiveRemoteUpdateMessage({ path, mime, state });
827
+ _failsCallbacks.postMessageToFails!({
828
+ requestId: event.data.requestId,
829
+ task: 'receiveInterceptorUpdate'
830
+ });
831
+ }
832
+ break;
833
+ case 'restartKernelAndRerunCells':
834
+ {
835
+ if (typeof currentDocWidget === 'undefined') {
836
+ _failsCallbacks.postMessageToFails!({
837
+ requestId: event.data.requestId,
838
+ task: 'restartKernelAndRerunCell',
839
+ error: 'No document loaded'
840
+ });
841
+ break;
842
+ }
843
+ const notebookPanel = currentDocWidget as NotebookPanel;
844
+ const { context, content } = notebookPanel;
845
+ const cells = content.widgets;
846
+ console.log('rerun kernel hook');
847
+
848
+ notebookPanel.sessionContext
849
+ .restartKernel()
850
+ .then(async () => {
851
+ await NotebookActions.runCells(
852
+ content,
853
+ cells,
854
+ context.sessionContext
855
+ );
856
+ _failsCallbacks.postMessageToFails!({
857
+ requestId: event.data.requestId,
858
+ task: 'restartKernelAndRerunCell',
859
+ success: true
860
+ });
861
+ })
862
+ .catch((error: Error) => {
863
+ _failsCallbacks.postMessageToFails!({
864
+ requestId: event.data.requestId,
865
+ task: 'restartKernelAndRerunCell',
866
+ error: error.toString()
867
+ });
868
+ });
869
+ }
870
+ break;
871
+ case 'getLicenses':
872
+ {
873
+ ServerConnection.makeRequest(licensesUrl, {}, serverSettings)
874
+ .then(async response => {
875
+ const json = await response.json();
876
+ _failsCallbacks.postMessageToFails!({
877
+ requestId: event.data.requestId,
878
+ task: 'getLicenses',
879
+ licenses: json
880
+ });
881
+ })
882
+ .catch(error => {
883
+ _failsCallbacks.postMessageToFails!({
884
+ requestId: event.data.requestId,
885
+ task: 'getLicenses',
886
+ error: error.toString()
887
+ });
888
+ });
889
+ }
890
+ break;
891
+ }
892
+ });
893
+
894
+ app.started.then(async () => {
895
+ if (window.parent) {
896
+ window.parent.postMessage(
897
+ {
898
+ task: 'appLoaded'
899
+ },
900
+ '*' // this is relatively safe, as we only say we are ready
901
+ );
902
+ }
903
+ if (shell) {
904
+ // we have a notebook
905
+ shell.collapseTop();
906
+ if (failsLauncherInfo.inLecture) {
907
+ const menuWrapper = shell['_menuWrapper'] as Panel;
908
+ menuWrapper.hide();
909
+ // const main = shell['_main'] as SplitViewNotebookPanel;
910
+ // main.toolbar.hide();
911
+ }
912
+ }
913
+ });
914
+ return failsLauncherInfo;
915
+ }
916
+
917
+ const failsLauncher: JupyterFrontEndPlugin<IFailsLauncherInfo> = {
918
+ id: '@fails-components/jupyter-fails:launcher',
919
+ description: 'Configures the notebooks application over messages',
920
+ autoStart: true,
921
+ activate: activateFailsLauncher,
922
+ provides: IFailsLauncherInfo,
923
+ requires: [IDocumentManager, ILabStatus],
924
+ optional: [INotebookShell]
925
+ };
926
+
927
+ /**
928
+ * Initialization data for the @fails-components/jupyter-launcher extension.
929
+ */
930
+ const plugins: JupyterFrontEndPlugin<any>[] = [
931
+ // all JupyterFrontEndPlugins
932
+
933
+ failsLauncher
934
+ ];
935
+
936
+ export default plugins;