Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sagecell
Path: blob/master/js/session.js
447 views
1
import $ from "jquery";
2
import sagecell from "./sagecell";
3
4
import IPython from "base/js/namespace";
5
import events from "base/js/events";
6
import Kernel from "services/kernels/kernel";
7
import InteractCell from "./interact_cell";
8
import interact_controls from "./interact_controls";
9
import MultiSockJS from "./multisockjs";
10
import utils from "./utils";
11
import widgets from "./widgets";
12
13
import { URLs } from "./urls";
14
import { console } from "./console";
15
16
Kernel.Kernel.prototype.kill = function () {
17
utils.sendRequest("DELETE", this.kernel_url);
18
};
19
20
var ce = utils.createElement;
21
22
var interacts = {};
23
24
var stop = function (event) {
25
event.stopPropagation();
26
};
27
var close = null;
28
29
var jmolCounter = 0;
30
31
export function Session(outputDiv, language, interact_vals, k, linked) {
32
this.timer = utils.simpleTimer();
33
this.outputDiv = outputDiv;
34
this.outputDiv[0].sagecell_session = this;
35
this.language = language;
36
this.interact_vals = interact_vals;
37
this.linked = linked;
38
this.last_requests = {};
39
this.sessionContinue = true;
40
this.namespaces = {};
41
42
// Set this object because we aren't loading the full IPython JavaScript library
43
IPython.notification_widget = { set_message: console.debug };
44
45
this.interacts = [];
46
if (window.addEventListener) {
47
// Prevent Esc key from closing WebSockets and XMLHttpRequests in Firefox
48
window.addEventListener("keydown", function (event) {
49
if (event.keyCode === 27) {
50
event.preventDefault();
51
}
52
});
53
}
54
/* Always use sockjs, until we can get websockets working reliably.
55
* Right now, if we have a very short computation (like 1+1), there is some sort of
56
* race condition where the iopub handler does not get established before
57
* the kernel is closed down. This only manifests itself on a remote server, since presumably
58
* if you are running on a local server, the connection is established too quickly.
59
*
60
* Also, there are some bugs in, for example, Firefox and other issues that we don't want to have
61
* to work around, that sockjs already worked around.
62
*/
63
var that = this;
64
if (linked && sagecell.kernels[k]) {
65
this.kernel = sagecell.kernels[k];
66
} else {
67
var old_ws = window.WebSocket;
68
window.WebSocket = MultiSockJS;
69
// sometimes (IE8) window.console is not defined (until the console is opened)
70
window.console = window.console || {};
71
this.kernel = sagecell.kernels[k] = new Kernel.Kernel(URLs.kernel);
72
this.kernel.comm_manager.register_target(
73
"threejs",
74
utils.always_new(widgets.ThreeJS(this))
75
);
76
this.kernel.comm_manager.register_target(
77
"graphicswidget",
78
utils.always_new(widgets.Graphics(this))
79
);
80
this.kernel.comm_manager.register_target(
81
"matplotlib",
82
utils.always_new(widgets.MPL(this))
83
);
84
85
this.kernel.session = this;
86
this.kernel.opened = false;
87
this.kernel.deferred_code = [];
88
window.WebSocket = old_ws;
89
90
this.kernel.post = function (url, callback) {
91
utils.sendRequest("POST", url, {}, function (data) {
92
callback(JSON.parse(data));
93
});
94
};
95
96
// Copied from Jupyter notebook and slightly modified to add deferred code execution
97
this.kernel._ws_opened = function (evt) {
98
/**
99
* Handle a websocket entering the open state,
100
* signaling that the kernel is connected when websocket is open.
101
*
102
* @function _ws_opened
103
*/
104
if (this.is_connected()) {
105
// ADDED BLOCK START
106
this.opened = true;
107
while (this.deferred_code.length > 0) {
108
this.session.execute(this.deferred_code.shift());
109
}
110
// ADDED BLOCK END
111
// all events ready, trigger started event.
112
this._kernel_connected();
113
}
114
};
115
116
this.kernel.start({
117
CellSessionID: utils.cellSessionID(),
118
timeout: linked ? "inf" : 0,
119
accepted_tos: "true",
120
});
121
}
122
var pl_button, pl_box, pl_zlink, pl_qlink, pl_qrcode, pl_chkbox;
123
this.outputDiv.find(".sagecell_output").prepend(
124
(this.session_container = ce(
125
"div",
126
{ class: "sagecell_sessionContainer" },
127
[
128
ce("div", { class: "sagecell_permalink" }, [
129
(pl_button = ce("button", {}, ["Share"])),
130
(pl_box = ce(
131
"div",
132
{ class: "sagecell_permalink_result" },
133
[
134
ce("div", {}, [
135
(pl_zlink = ce(
136
"a",
137
{
138
title: "Link that will work on any Sage Cell server",
139
},
140
["Permalink"]
141
)),
142
]),
143
ce("div", {}, [
144
(pl_qlink = ce(
145
"a",
146
{
147
title: "Shortened link that will only work on this server",
148
},
149
["Short temporary link"]
150
)),
151
]),
152
ce("div", {}, [
153
ce("a", {}, [
154
(pl_qrcode = ce("img", {
155
title: "QR code that will only work on this server",
156
alt: "",
157
})),
158
]),
159
]),
160
(this.interact_pl = ce("label", {}, [
161
"Share interact state",
162
(pl_chkbox = ce("input", {
163
type: "checkbox",
164
})),
165
])),
166
]
167
)),
168
]),
169
(this.output_block = ce(
170
"div",
171
{ class: "sagecell_sessionOutput sagecell_active" },
172
[
173
(this.spinner = ce("img", {
174
src: URLs.spinner,
175
alt: "Loading",
176
class: "sagecell_spinner",
177
})),
178
]
179
)),
180
ce("div", { class: "sagecell_poweredBy" }, [
181
ce("a", { href: URLs.help, target: "_blank" }, ["Help"]),
182
" | Powered by ",
183
ce(
184
"a",
185
{
186
href: "http://www.sagemath.org",
187
target: "_blank",
188
},
189
["SageMath"]
190
),
191
]),
192
(this.session_files = ce("div", {
193
class: "sagecell_sessionFiles",
194
})),
195
]
196
))
197
);
198
pl_box.style.display = this.interact_pl.style.display = "none";
199
var pl_hidden = true;
200
var hide_box = function hide_box() {
201
pl_box.style.display = "none";
202
window.removeEventListener("mousedown", hide_box);
203
pl_hidden = true;
204
close = null;
205
};
206
var n = 0;
207
var code_links = {},
208
interact_links = {};
209
var that = this;
210
var qr_prefix =
211
"https://quickchart.io/qr?ecLevel=H&size=200&format=svg&text=";
212
this.updateLinks = function (new_vals) {
213
if (new_vals) {
214
interact_links = {};
215
}
216
if (pl_hidden) {
217
return;
218
}
219
var links = pl_chkbox.checked ? interact_links : code_links;
220
if (links.zip === undefined) {
221
pl_zlink.removeAttribute("href");
222
pl_qlink.removeAttribute("href");
223
pl_qrcode.parentNode.removeAttribute("href");
224
pl_qrcode.removeAttribute("src");
225
console.debug("sending permalink request post:", that.timer());
226
var args = {
227
code: that.rawcode,
228
language: that.language,
229
n: ++n,
230
};
231
if (pl_chkbox.checked) {
232
var list = [];
233
for (var i = 0; i < that.interacts.length; i++) {
234
if (that.interacts[i].parent_block === null) {
235
var interact = that.interacts[i];
236
var dict = {
237
state: interact.state(),
238
bookmarks: [],
239
};
240
for (
241
var j = 0;
242
j < interact.bookmarks.childNodes.length;
243
j++
244
) {
245
var b = interact.bookmarks.childNodes[j];
246
if (b.firstChild.firstChild.hasChildNodes()) {
247
dict.bookmarks.push({
248
name: b.firstChild.firstChild.firstChild
249
.nodeValue,
250
state: $(b).data("values"),
251
});
252
}
253
}
254
list.push(dict);
255
}
256
}
257
args.interacts = JSON.stringify(list);
258
}
259
utils.sendRequest("POST", URLs.permalink, args, function (data) {
260
data = JSON.parse(data);
261
console.debug("POST permalink request:", that.timer());
262
if (data.n !== n) {
263
return;
264
}
265
pl_qlink.href = links.query = URLs.root + "?q=" + data.query;
266
links.zip =
267
URLs.root + "?z=" + data.zip + "&lang=" + that.language;
268
if (data.interacts) {
269
links.zip += "&interacts=" + data.interacts;
270
}
271
pl_zlink.href = links.zip;
272
pl_qrcode.parentNode.href = links.query;
273
pl_qrcode.src = qr_prefix + links.query;
274
});
275
} else {
276
pl_qlink.href = pl_qrcode.parentNode.href = links.query;
277
pl_zlink.href = links.zip;
278
pl_qrcode.src = qr_prefix + links.query;
279
}
280
};
281
pl_button.addEventListener("click", function () {
282
if (pl_hidden) {
283
pl_hidden = false;
284
that.updateLinks(false);
285
pl_box.style.display = "block";
286
if (close) {
287
close();
288
}
289
close = hide_box;
290
window.addEventListener("mousedown", hide_box);
291
} else {
292
hide_box();
293
}
294
});
295
pl_button.addEventListener("mousedown", stop);
296
pl_box.addEventListener("mousedown", stop);
297
events.on("kernel_busy.Kernel", function (evt, data) {
298
console.debug("kernel_busy.Kernel for", data.kernel.id);
299
if (data.kernel.id === that.kernel.id) {
300
that.spinner.style.display = "";
301
}
302
});
303
pl_chkbox.addEventListener("change", function () {
304
that.updateLinks(false);
305
});
306
events.on("kernel_idle.Kernel", function (evt, data) {
307
console.debug("kernel_idle.Kernel for", data.kernel.id);
308
if (data.kernel.id !== that.kernel.id) {
309
return;
310
}
311
that.spinner.style.display = "none";
312
for (var i = 0, j = 0; i < that.interact_vals.length; i++) {
313
while (
314
that.interacts[j] &&
315
that.interacts[j].parent_block !== null
316
) {
317
j++;
318
}
319
if (j === that.interacts.length) {
320
break;
321
}
322
that.interacts[j].state(
323
that.interact_vals[i].state,
324
(function (interact, val) {
325
return function () {
326
interact.clearBookmarks();
327
for (var i = 0; i < val.bookmarks.length; i++) {
328
interact.createBookmark(
329
val.bookmarks[i].name,
330
val.bookmarks[i].state
331
);
332
}
333
};
334
})(that.interacts[j], that.interact_vals[i])
335
);
336
j++;
337
}
338
that.interact_vals = [];
339
});
340
var killkernel = function (evt, data) {
341
console.debug("killkernel for", data.kernel.id);
342
if (data.kernel.id === that.kernel.id) {
343
that.spinner.style.display = "none";
344
for (var i = 0; i < that.interacts.length; i++) {
345
that.interacts[i].disable();
346
}
347
$(that.output_block).removeClass("sagecell_active");
348
data.kernel.shell_channel = {};
349
data.kernel.iopub_channel = {};
350
sagecell.kernels[k] = null;
351
}
352
};
353
events.on("kernel_dead.Kernel", killkernel);
354
events.on("kernel_disconnected.Kernel", killkernel);
355
this.lock_output = false;
356
this.files = {};
357
this.eventHandlers = {};
358
}
359
360
Session.prototype.send_message = function () {
361
this.kernel.send_shell_message.apply(this.kernel, arguments);
362
};
363
364
Session.prototype.execute = function (code) {
365
if (this.kernel.opened) {
366
console.debug("opened and executing in kernel:", this.timer());
367
//TODO: do this wrapping of code on the server, not in javascript
368
//Maybe the system can be sent in metadata in the execute_request message
369
this.rawcode = code;
370
// Modifying code in chosen language
371
if (this.language === "octave") {
372
code =
373
"warning('off', 'Octave:gnuplot-graphics')\n" +
374
"set(gcf(), 'visible', 'off')\n" +
375
code +
376
"\n" +
377
"if (get(gcf(), 'children'))\n" +
378
" saveas(gcf(), 'octave.png')\n" +
379
" close\n" +
380
"endif";
381
}
382
// Converting code into Python expression
383
if (this.language !== "sage") {
384
code =
385
'("""' +
386
code.replace(/\\/g, "\\\\").replace(/"/g, '\\"') +
387
'""")';
388
if (this.language === "python") {
389
code = "exec" + code;
390
} else if (this.language === "html") {
391
code = "html" + code;
392
} else {
393
code = "print(" + this.language + ".eval" + code + ")";
394
}
395
}
396
// Modifying Python expression
397
if (this.language === "octave") {
398
code =
399
"if octave.path() != os.getcwd():\n octave = Octave()\n" +
400
code;
401
}
402
if (this.language === "r") {
403
code =
404
"r.eval(\"options(bitmapType='cairo')\"); " +
405
code +
406
'\nr.eval("graphics.off()"); None';
407
}
408
this.code = code;
409
var callbacks = {
410
iopub: { output: $.proxy(this.handle_output, this) },
411
shell: { reply: $.proxy(this.handle_execute_reply, this) },
412
};
413
this.set_last_request(
414
null,
415
this.kernel.execute(code, callbacks, {
416
silent: false,
417
user_expressions: {
418
_sagecell_files: "sys._sage_.new_files()",
419
},
420
})
421
);
422
} else {
423
this.kernel.deferred_code.push(code);
424
}
425
};
426
427
Session.prototype.set_last_request = function (interact_id, msg_id) {
428
this.kernel.set_callbacks_for_msg(this.last_requests[interact_id]);
429
this.last_requests[interact_id] = msg_id;
430
};
431
432
Session.prototype.appendMsg = function (msg, text) {
433
// Append the message to the div of messages
434
// Use $.text() so that strings are automatically escaped
435
$(ce("div"))
436
.text(text + JSON.stringify(msg))
437
.prependTo(this.outputDiv.find(".sagecell_messages"));
438
};
439
440
Session.prototype.clear = function (block_id, changed) {
441
var output_block = $(
442
block_id === null ? this.output_block : interacts[block_id].output_block
443
);
444
if (output_block.length === 0) {
445
return;
446
}
447
output_block[0].style.minHeight = output_block.height() + "px";
448
setTimeout(function () {
449
output_block.animate({ "min-height": "0px" }, "slow");
450
}, 3000);
451
output_block.empty();
452
if (changed) {
453
for (var i = 0; i < changed.length; i++) {
454
$(interacts[block_id].cells[changed[i]]).removeClass(
455
"sagecell_dirtyControl"
456
);
457
}
458
}
459
for (var i = 0; i < this.interacts.length; i++) {
460
if (this.interacts[i].parent_block === block_id) {
461
this.clear(this.interacts[i].interact_id);
462
delete interacts[this.interacts[i].interact_id];
463
this.interacts.splice(i--, 1);
464
}
465
}
466
};
467
468
Session.prototype.output = function (html, block_id) {
469
// Return a DOM element for new content. The html is appended to the html
470
// block and the newly appended content element is returned.
471
var output_block = $(
472
block_id === null ? this.output_block : interacts[block_id].output_block
473
);
474
if (output_block.length !== 0) {
475
return $(html).appendTo(output_block);
476
}
477
};
478
479
Session.prototype.handle_message_reply = function (msg) {};
480
481
Session.prototype.handle_execute_reply = function (msg) {
482
console.debug("handle_execute_reply:", this.timer());
483
/* This would give two error messages (since a pyerr should have already come)
484
if(msg.status==="error") {
485
this.output('<pre class="sagecell_pyerr"></pre>',null)
486
.html(utils.fixConsole(msg.traceback.join("\n")));
487
}
488
*/
489
// TODO: handle payloads with a payload callback, instead of in the execute_reply
490
// That would be much less brittle
491
var payload = msg.content.payload[0];
492
if (!payload) {
493
return;
494
}
495
if (payload.new_files && payload.new_files.length > 0) {
496
var files = payload.new_files;
497
var output_block = this.outputDiv.find("div.sagecell_sessionFiles");
498
var html = "<div>\n";
499
for (var j = 0, j_max = files.length; j < j_max; j++) {
500
if (this.files[files[j]] !== undefined) {
501
this.files[files[j]]++;
502
} else {
503
this.files[files[j]] = 0;
504
}
505
}
506
var filepath = this.kernel.kernel_url + "/files/";
507
for (j in this.files) {
508
//TODO: escape filenames and id
509
html +=
510
'<a href="' +
511
filepath +
512
j +
513
"?q=" +
514
this.files[j] +
515
'" target="_blank">' +
516
j +
517
"</a> [Updated " +
518
this.files[j] +
519
" time(s)]<br>\n";
520
}
521
html += "</div>";
522
output_block.html(html).effect("pulsate", { times: 1 }, 500);
523
}
524
if (payload.data && payload.data["text/plain"]) {
525
this.output('<pre class="sagecell_payload"></pre>', null).html(
526
utils.fixConsole(payload.data["text/plain"])
527
);
528
}
529
};
530
531
Session.prototype.handle_output = function (msg, default_block_id) {
532
console.debug("handle_output");
533
var msg_type = msg.header.msg_type;
534
var content = msg.content;
535
var metadata = msg.metadata;
536
var block_id = metadata.interact_id || default_block_id || null;
537
if (block_id !== null && !interacts.hasOwnProperty(block_id)) {
538
return;
539
}
540
// Handle each stream type. This should probably be separated out into different functions.
541
switch (msg_type) {
542
case "stream":
543
// First, see if we should consolidate this output with the previous output <pre>
544
// this reaches into the inner workings of output
545
var block = $(
546
block_id === null
547
? this.output_block
548
: interacts[block_id].output_block
549
);
550
var last = block.children().last();
551
var last_output = last.length === 0 ? undefined : last;
552
if (
553
last_output &&
554
last_output.hasClass("sagecell_" + content.name)
555
) {
556
last_output.text(last_output.text() + content.text);
557
} else {
558
var html = ce("pre", { class: "sagecell_" + content.name }, [
559
content.text,
560
]);
561
this.output(html, block_id);
562
}
563
break;
564
case "error":
565
if (content.traceback.join) {
566
this.output(
567
'<pre class="sagecell_pyerr"></pre>',
568
block_id
569
).html(utils.fixConsole(content.traceback.join("\n")));
570
}
571
break;
572
case "display_data":
573
case "execute_result":
574
var filepath = this.kernel.kernel_url + "/files/";
575
// find any key of content that is in the display_handlers array and execute that handler
576
// if none found, do the text/plain
577
var already_handled = false;
578
for (var key in content.data) {
579
if (
580
content.data.hasOwnProperty(key) &&
581
this.display_handlers[key]
582
) {
583
// return false if the mime type wasn't handled after all
584
already_handled =
585
false !==
586
$.proxy(this.display_handlers[key], this)(
587
content.data[key],
588
block_id,
589
filepath
590
);
591
// we only use one mime type
592
break;
593
}
594
}
595
if (!already_handled && content.data["text/plain"]) {
596
// we are *always* supposed to have a text/plain attribute
597
this.output("<pre></pre>", block_id).text(
598
content.data["text/plain"]
599
);
600
}
601
break;
602
}
603
console.debug("handled output:", this.timer());
604
this.appendMsg(content, "Accepted: ");
605
// need to mathjax the entire output, since output_block could just be part of the output
606
var output = this.outputDiv.find(".sagecell_output").get(0);
607
if (MathJax.version.startsWith("2")) {
608
// MathJax 2
609
MathJax.Hub.Queue(["Typeset", MathJax.Hub, output]);
610
MathJax.Hub.Queue([
611
function () {
612
$(output).find(".math").removeClass("math");
613
},
614
]);
615
} else {
616
// MathJax 3
617
MathJax.typesetPromise([output]);
618
}
619
};
620
621
// dispatch table on mime type
622
Session.prototype.display_handlers = {
623
"application/sage-interact": function (data, block_id) {
624
this.interacts.push(
625
(interacts[data.new_interact_id] = new InteractCell(
626
this,
627
data,
628
block_id
629
))
630
);
631
},
632
"application/sage-interact-update": function (data) {
633
interacts[data.interact_id].updateControl(data);
634
},
635
"application/sage-interact-new-control": function (data) {
636
interacts[data.interact_id].newControl(data);
637
},
638
"application/sage-interact-del-control": function (data) {
639
interacts[data.interact_id].delControl(data);
640
},
641
"application/sage-interact-bookmark": function (data) {
642
interacts[data.interact_id].createBookmark(data.name, data.values);
643
},
644
"application/sage-interact-control": function (data, block_id) {
645
var that = this;
646
var control_class = interact_controls[data.control_type];
647
if (control_class === undefined) {
648
return false;
649
}
650
var control = new control_class(this, data.control_id);
651
control.create(data, block_id);
652
$.each(data.variable, function (index, value) {
653
that.register_control(data.namespace, value, control);
654
});
655
control.update(data.namespace, data.variable);
656
},
657
"application/sage-interact-variable": function (data) {
658
this.update_variable(data.namespace, data.variable, data.control);
659
},
660
"application/sage-clear": function (data, block_id) {
661
this.clear(block_id, data.changed);
662
},
663
"text/html": function (data, block_id, filepath) {
664
this.output("<div></div>", block_id).html(
665
data.replace(/cell:\/\//gi, filepath)
666
);
667
},
668
"application/javascript": function (data) {
669
eval(data);
670
},
671
"text/image-filename": function (data, block_id, filepath) {
672
this.output("<img src='" + filepath + data + "'/>", block_id);
673
},
674
"image/png": function (data, block_id) {
675
this.output(
676
"<img src='data:image/png;base64," + data + "'/>",
677
block_id
678
);
679
},
680
"application/x-jmol": function (data, block_id, filepath) {
681
Jmol.setDocument(false);
682
var info = {
683
height: 500,
684
width: 500,
685
color: "white",
686
j2sPath: URLs.root + "static/jsmol/j2s",
687
serverURL: URLs.root + "static/jsmol/php/jsmol.php",
688
coverImage: filepath + data + "/preview.png",
689
deferUncover: true,
690
disableInitialConsole: true,
691
script:
692
"set defaultdirectory '" +
693
filepath +
694
data +
695
"/scene.zip';\n script SCRIPT;\n",
696
menuFile: URLs.root + "static/SageMenu.mnu",
697
};
698
this.output(
699
Jmol.getAppletHtml("scJmol" + jmolCounter++, info),
700
block_id
701
);
702
},
703
"application/x-canvas3d": function (data, block_id, filepath) {
704
var div = this.output(document.createElement("div"), block_id);
705
var old_cw = [window.hasOwnProperty("cell_writer"), window.cell_writer],
706
old_tr = [
707
window.hasOwnProperty("translations"),
708
window.translations,
709
];
710
window.cell_writer = {
711
write: function (html) {
712
div.html(html);
713
},
714
};
715
var text =
716
"Sorry, but you need a browser that supports the &lt;canvas&gt; tag.";
717
window.translations = {};
718
window.translations[text] = text;
719
canvas3d.viewer(filepath + data);
720
if (old_cw[0]) {
721
window.cell_writer = old_cw[1];
722
} else {
723
delete window.cell_writer;
724
}
725
if (old_tr[0]) {
726
window.translations = old_tr[1];
727
} else {
728
delete window.translations;
729
}
730
},
731
};
732
733
Session.prototype.register_control = function (namespace, variable, control) {
734
if (this.namespaces[namespace] === undefined) {
735
this.namespaces[namespace] = {};
736
}
737
if (this.namespaces[namespace][variable] === undefined) {
738
this.namespaces[namespace][variable] = [];
739
}
740
this.namespaces[namespace][variable].push(control);
741
};
742
743
Session.prototype.get_variable_controls = function (namespace, variable) {
744
var notify = {};
745
if (this.namespaces[namespace] && this.namespaces[namespace][variable]) {
746
$.each(this.namespaces[namespace][variable], function (index, control) {
747
notify[control.control_id] = control;
748
});
749
}
750
return notify;
751
};
752
753
Session.prototype.update_variable = function (namespace, variable, control_id) {
754
var that = this;
755
var notify;
756
if ($.isArray(variable)) {
757
notify = {};
758
$.each(variable, function (index, v) {
759
$.extend(notify, that.get_variable_controls(namespace, v));
760
});
761
} else {
762
notify = this.get_variable_controls(namespace, variable);
763
}
764
$.each(notify, function (k, v) {
765
$.proxy(v.update, v)(namespace, variable, control_id);
766
});
767
};
768
769
Session.prototype.destroy = function () {
770
this.clear(null);
771
$(this.session_container).remove();
772
};
773
774
export default Session;
775
776