@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/LICENSE +29 -0
- package/README.md +101 -0
- package/lib/index.d.ts +139 -0
- package/lib/index.js +631 -0
- package/package.json +214 -0
- package/src/index.ts +936 -0
- package/style/base.css +5 -0
- package/style/index.css +1 -0
- package/style/index.js +2 -0
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;
|