Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/smc_sagews/smc_sagews/sage_server.py
Views: 286
1
#!/usr/bin/env python
2
"""
3
sage_server.py -- unencrypted forking TCP server.
4
5
Note: I wrote functionality so this can run as root, create accounts on the fly,
6
and serve sage as those accounts. Doing this is horrendous from a security point of
7
view, and I'm definitely not doing this.
8
9
None of that functionality is actually used in https://cocalc.com!
10
11
For debugging, this may help:
12
13
killemall sage_server.py && sage --python sage_server.py -p 6000
14
15
"""
16
17
# NOTE: This file is GPL'd
18
# because it imports the Sage library. This file is not directly
19
# imported by anything else in CoCalc; the Python process it runs is
20
# used over a TCP connection.
21
22
#########################################################################################
23
# Copyright (C) 2016, Sagemath Inc.
24
# #
25
# Distributed under the terms of the GNU General Public License (GPL), version 2+ #
26
# #
27
# http://www.gnu.org/licenses/ #
28
#########################################################################################
29
30
# Add the path that contains this file to the Python load path, so we
31
# can import other files from there.
32
from __future__ import absolute_import
33
import six
34
import os, sys, time, operator
35
import __future__ as future
36
from functools import reduce
37
38
39
def is_string(s):
40
return isinstance(s, six.string_types)
41
42
43
def unicode8(s):
44
# I evidently don't understand Python unicode... Do the following for now:
45
# TODO: see http://stackoverflow.com/questions/21897664/why-does-unicodeu-passed-an-errors-parameter-raise-typeerror for how to fix.
46
try:
47
if six.PY2:
48
return str(s).encode('utf-8')
49
else:
50
return str(s, 'utf-8')
51
except:
52
try:
53
return str(s)
54
except:
55
return s
56
57
58
LOGFILE = os.path.realpath(__file__)[:-3] + ".log"
59
PID = os.getpid()
60
from datetime import datetime
61
62
63
def log(*args):
64
try:
65
debug_log = open(LOGFILE, 'a')
66
mesg = "%s (%s): %s\n" % (PID, datetime.utcnow().strftime(
67
'%Y-%m-%d %H:%M:%S.%f')[:-3], ' '.join([unicode8(x)
68
for x in args]))
69
debug_log.write(mesg)
70
debug_log.flush()
71
except Exception as err:
72
print(("an error writing a log message (ignoring) -- %s" % err, args))
73
74
75
# used for clearing pylab figure
76
pylab = None
77
78
# Maximum number of distinct (non-once) output messages per cell; when this number is
79
# exceeded, an exception is raised; this reduces the chances of the user creating
80
# a huge unusable worksheet.
81
MAX_OUTPUT_MESSAGES = 256
82
# stdout, stderr, html, etc. that exceeds this many characters will be truncated to avoid
83
# killing the client.
84
MAX_STDOUT_SIZE = MAX_STDERR_SIZE = MAX_CODE_SIZE = MAX_HTML_SIZE = MAX_MD_SIZE = MAX_TEX_SIZE = 40000
85
86
MAX_OUTPUT = 150000
87
88
# Standard imports.
89
import json, resource, shutil, signal, socket, struct, \
90
tempfile, time, traceback, pwd, re
91
92
# for "3x^2 + 4xy - 5(1+x) - 3 abc4ok", this pattern matches "3x", "5(" and "4xy" but not "abc4ok"
93
# to understand it, see https://regex101.com/ or https://www.debuggex.com/
94
RE_POSSIBLE_IMPLICIT_MUL = re.compile(r'(?:(?<=[^a-zA-Z])|^)(\d+[a-zA-Z\(]+)')
95
96
try:
97
from . import sage_parsing, sage_salvus
98
except:
99
import sage_parsing, sage_salvus
100
101
uuid = sage_salvus.uuid
102
103
reload_attached_files_if_mod_smc_available = True
104
105
106
def reload_attached_files_if_mod_smc():
107
# CRITICAL: do NOT impor sage.repl.attach!! That will import IPython, wasting several seconds and
108
# killing the user experience for no reason.
109
try:
110
import sage.repl
111
sage.repl.attach
112
except:
113
# nothing to do -- attach has not been used and is not yet available.
114
return
115
global reload_attached_files_if_mod_smc_available
116
if not reload_attached_files_if_mod_smc_available:
117
return
118
try:
119
from sage.repl.attach import load_attach_path, modified_file_iterator
120
except:
121
print("sage_server: attach not available")
122
reload_attached_files_if_mod_smc_available = False
123
return
124
# see sage/src/sage/repl/attach.py reload_attached_files_if_modified()
125
for filename, mtime in modified_file_iterator():
126
basename = os.path.basename(filename)
127
timestr = time.strftime('%T', mtime)
128
log('reloading attached file {0} modified at {1}'.format(
129
basename, timestr))
130
from .sage_salvus import load
131
load(filename)
132
133
134
# Determine the info object, if available. There's no good reason
135
# it wouldn't be available, unless a user explicitly deleted it, but
136
# we may as well try to be robust to this, especially if somebody
137
# were to try to use this server outside of cloud.sagemath.com.
138
_info_path = os.path.join(os.environ['SMC'], 'info.json')
139
if os.path.exists(_info_path):
140
try:
141
INFO = json.loads(open(_info_path).read())
142
except:
143
# This will fail, e.g., if info.json is invalid (maybe a blank file).
144
# We definitely don't want sage server startup to be completely broken
145
# in this case, so we fall back to "no info".
146
INFO = {}
147
else:
148
INFO = {}
149
if 'base_url' not in INFO:
150
INFO['base_url'] = ''
151
152
# Configure logging
153
#logging.basicConfig()
154
#log = logging.getLogger('sage_server')
155
#log.setLevel(logging.INFO)
156
157
# A CoffeeScript version of this function is in misc_node.coffee.
158
import hashlib
159
160
161
def uuidsha1(data):
162
sha1sum = hashlib.sha1()
163
sha1sum.update(data)
164
s = sha1sum.hexdigest()
165
t = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
166
r = list(t)
167
j = 0
168
for i in range(len(t)):
169
if t[i] == 'x':
170
r[i] = s[j]
171
j += 1
172
elif t[i] == 'y':
173
# take 8 + low order 3 bits of hex number.
174
r[i] = hex((int(s[j], 16) & 0x3) | 0x8)[-1]
175
j += 1
176
return ''.join(r)
177
178
179
# A tcp connection with support for sending various types of messages, especially JSON.
180
class ConnectionJSON(object):
181
182
def __init__(self, conn):
183
# avoid common mistake -- conn is supposed to be from socket.socket...
184
assert not isinstance(conn, ConnectionJSON)
185
self._conn = conn
186
187
def close(self):
188
self._conn.close()
189
190
def _send(self, s):
191
if six.PY3 and type(s) == str:
192
s = s.encode('utf8')
193
length_header = struct.pack(">L", len(s))
194
# py3: TypeError: can't concat str to bytes
195
self._conn.send(length_header + s)
196
197
def send_json(self, m):
198
m = json.dumps(m)
199
if '\\u0000' in m:
200
raise RuntimeError("NULL bytes not allowed")
201
log("sending message '", truncate_text(m, 256), "'")
202
self._send('j' + m)
203
return len(m)
204
205
def send_blob(self, blob):
206
if six.PY3 and type(blob) == str:
207
# unicode objects must be encoded before hashing
208
blob = blob.encode('utf8')
209
210
s = uuidsha1(blob)
211
if six.PY3 and type(blob) == bytes:
212
# we convert all to bytes first, to avoid unnecessary conversions
213
self._send(('b' + s).encode('utf8') + blob)
214
else:
215
# old sage py2 code
216
self._send('b' + s + blob)
217
return s
218
219
def send_file(self, filename):
220
log("sending file '%s'" % filename)
221
f = open(filename, 'rb')
222
data = f.read()
223
f.close()
224
return self.send_blob(data)
225
226
def _recv(self, n):
227
#print("_recv(%s)"%n)
228
# see http://stackoverflow.com/questions/3016369/catching-blocking-sigint-during-system-call
229
for i in range(20):
230
try:
231
#print "blocking recv (i = %s), pid=%s"%(i, os.getpid())
232
r = self._conn.recv(n)
233
#log("n=%s; received: '%s' of len %s"%(n,r, len(r)))
234
return r
235
except OSError as e:
236
#print("socket.error, msg=%s"%msg)
237
if e.errno != 4:
238
raise
239
raise EOFError
240
241
def recv(self):
242
n = self._recv(4)
243
if len(n) < 4:
244
raise EOFError
245
n = struct.unpack('>L', n)[0] # big endian 32 bits
246
s = self._recv(n)
247
while len(s) < n:
248
t = self._recv(n - len(s))
249
if len(t) == 0:
250
raise EOFError
251
s += t
252
253
if six.PY3:
254
# bystream to string, in particular s[0] will be e.g. 'j' and not 106
255
#log("ConnectionJSON::recv s=%s... (type %s)" % (s[:5], type(s)))
256
# is s always of type bytes?
257
if type(s) == bytes:
258
s = s.decode('utf8')
259
260
if s[0] == 'j':
261
try:
262
return 'json', json.loads(s[1:])
263
except Exception as msg:
264
log("Unable to parse JSON '%s'" % s[1:])
265
raise
266
267
elif s[0] == 'b':
268
return 'blob', s[1:]
269
raise ValueError("unknown message type '%s'" % s[0])
270
271
272
def truncate_text(s, max_size):
273
if len(s) > max_size:
274
return s[:max_size] + "[...]", True
275
else:
276
return s, False
277
278
279
def truncate_text_warn(s, max_size, name):
280
r"""
281
Truncate text if too long and format a warning message.
282
283
INPUT:
284
285
- ``s`` -- string to be truncated
286
- ``max-size`` - integer truncation limit
287
- ``name`` - string, name of limiting parameter
288
289
OUTPUT:
290
291
a triple:
292
293
- string -- possibly truncated input string
294
- boolean -- true if input string was truncated
295
- string -- warning message if input string was truncated
296
"""
297
tmsg = "WARNING: Output: %s truncated by %s to %s. Type 'smc?' to learn how to raise the output limit."
298
lns = len(s)
299
if lns > max_size:
300
tmsg = tmsg % (lns, name, max_size)
301
return s[:max_size] + "[...]", True, tmsg
302
else:
303
return s, False, ''
304
305
306
class Message(object):
307
308
def _new(self, event, props={}):
309
m = {'event': event}
310
for key, val in props.items():
311
if key != 'self':
312
m[key] = val
313
return m
314
315
def start_session(self):
316
return self._new('start_session')
317
318
def session_description(self, pid):
319
return self._new('session_description', {'pid': pid})
320
321
def send_signal(self, pid, signal=signal.SIGINT):
322
return self._new('send_signal', locals())
323
324
def terminate_session(self, done=True):
325
return self._new('terminate_session', locals())
326
327
def execute_code(self, id, code, preparse=True):
328
return self._new('execute_code', locals())
329
330
def execute_javascript(self, code, obj=None, coffeescript=False):
331
return self._new('execute_javascript', locals())
332
333
def output(
334
self,
335
id,
336
stdout=None,
337
stderr=None,
338
code=None,
339
html=None,
340
javascript=None,
341
coffeescript=None,
342
interact=None,
343
md=None,
344
tex=None,
345
d3=None,
346
file=None,
347
raw_input=None,
348
obj=None,
349
once=None,
350
hide=None,
351
show=None,
352
events=None,
353
clear=None,
354
delete_last=None,
355
done=False # CRITICAL: done must be specified for multi-response; this is assumed by sage_session.coffee; otherwise response assumed single.
356
):
357
m = self._new('output')
358
m['id'] = id
359
t = truncate_text_warn
360
did_truncate = False
361
from . import sage_server # we do this so that the user can customize the MAX's below.
362
if code is not None:
363
code['source'], did_truncate, tmsg = t(code['source'],
364
sage_server.MAX_CODE_SIZE,
365
'MAX_CODE_SIZE')
366
m['code'] = code
367
if stderr is not None and len(stderr) > 0:
368
m['stderr'], did_truncate, tmsg = t(stderr,
369
sage_server.MAX_STDERR_SIZE,
370
'MAX_STDERR_SIZE')
371
if stdout is not None and len(stdout) > 0:
372
m['stdout'], did_truncate, tmsg = t(stdout,
373
sage_server.MAX_STDOUT_SIZE,
374
'MAX_STDOUT_SIZE')
375
if html is not None and len(html) > 0:
376
m['html'], did_truncate, tmsg = t(html, sage_server.MAX_HTML_SIZE,
377
'MAX_HTML_SIZE')
378
if md is not None and len(md) > 0:
379
m['md'], did_truncate, tmsg = t(md, sage_server.MAX_MD_SIZE,
380
'MAX_MD_SIZE')
381
if tex is not None and len(tex) > 0:
382
tex['tex'], did_truncate, tmsg = t(tex['tex'],
383
sage_server.MAX_TEX_SIZE,
384
'MAX_TEX_SIZE')
385
m['tex'] = tex
386
if javascript is not None: m['javascript'] = javascript
387
if coffeescript is not None: m['coffeescript'] = coffeescript
388
if interact is not None: m['interact'] = interact
389
if d3 is not None: m['d3'] = d3
390
if obj is not None: m['obj'] = json.dumps(obj)
391
if file is not None: m['file'] = file # = {'filename':..., 'uuid':...}
392
if raw_input is not None: m['raw_input'] = raw_input
393
if done is not None: m['done'] = done
394
if once is not None: m['once'] = once
395
if hide is not None: m['hide'] = hide
396
if show is not None: m['show'] = show
397
if events is not None: m['events'] = events
398
if clear is not None: m['clear'] = clear
399
if delete_last is not None: m['delete_last'] = delete_last
400
if did_truncate:
401
if 'stderr' in m:
402
m['stderr'] += '\n' + tmsg
403
else:
404
m['stderr'] = '\n' + tmsg
405
return m
406
407
def introspect_completions(self, id, completions, target):
408
m = self._new('introspect_completions', locals())
409
m['id'] = id
410
return m
411
412
def introspect_docstring(self, id, docstring, target):
413
m = self._new('introspect_docstring', locals())
414
m['id'] = id
415
return m
416
417
def introspect_source_code(self, id, source_code, target):
418
m = self._new('introspect_source_code', locals())
419
m['id'] = id
420
return m
421
422
423
message = Message()
424
425
whoami = os.environ['USER']
426
427
428
def client1(port, hostname):
429
conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
430
conn.connect((hostname, int(port)))
431
conn = ConnectionJSON(conn)
432
433
conn.send_json(message.start_session())
434
typ, mesg = conn.recv()
435
pid = mesg['pid']
436
print(("PID = %s" % pid))
437
438
id = 0
439
while True:
440
try:
441
code = sage_parsing.get_input('sage [%s]: ' % id)
442
if code is None: # EOF
443
break
444
conn.send_json(message.execute_code(code=code, id=id))
445
while True:
446
typ, mesg = conn.recv()
447
if mesg['event'] == 'terminate_session':
448
return
449
elif mesg['event'] == 'output':
450
if 'stdout' in mesg:
451
sys.stdout.write(mesg['stdout'])
452
sys.stdout.flush()
453
if 'stderr' in mesg:
454
print(('! ' +
455
'\n! '.join(mesg['stderr'].splitlines())))
456
if 'done' in mesg and mesg['id'] >= id:
457
break
458
id += 1
459
460
except KeyboardInterrupt:
461
print("Sending interrupt signal")
462
conn2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
463
conn2.connect((hostname, int(port)))
464
conn2 = ConnectionJSON(conn2)
465
conn2.send_json(message.send_signal(pid))
466
del conn2
467
id += 1
468
469
conn.send_json(message.terminate_session())
470
print("\nExiting Sage client.")
471
472
473
class BufferedOutputStream(object):
474
475
def __init__(self, f, flush_size=4096, flush_interval=.1):
476
self._f = f
477
self._buf = ''
478
self._flush_size = flush_size
479
self._flush_interval = flush_interval
480
self.reset()
481
482
def reset(self):
483
self._last_flush_time = time.time()
484
485
def fileno(self):
486
return 0
487
488
def write(self, output):
489
# CRITICAL: we need output to valid PostgreSQL TEXT, so no null bytes
490
# This is not going to silently corrupt anything -- it's just output that
491
# is destined to be *rendered* in the browser. This is only a partial
492
# solution to a more general problem, but it is safe.
493
try:
494
self._buf += output.replace('\x00', '')
495
except UnicodeDecodeError:
496
self._buf += output.decode('utf-8').replace('\x00', '')
497
#self.flush()
498
t = time.time()
499
if ((len(self._buf) >= self._flush_size)
500
or (t - self._last_flush_time >= self._flush_interval)):
501
self.flush()
502
self._last_flush_time = t
503
504
def flush(self, done=False):
505
if not self._buf and not done:
506
# no point in sending an empty message
507
return
508
try:
509
self._f(self._buf, done=done)
510
except UnicodeDecodeError:
511
if six.PY2: # str doesn't have errors option in python2!
512
self._f(unicode(self._buf, errors='replace'), done=done)
513
else:
514
self._f(str(self._buf, errors='replace'), done=done)
515
self._buf = ''
516
517
def isatty(self):
518
return False
519
520
521
# This will *have* to be re-done using Cython for speed.
522
class Namespace(dict):
523
524
def __init__(self, x):
525
self._on_change = {}
526
self._on_del = {}
527
dict.__init__(self, x)
528
529
def on(self, event, x, f):
530
if event == 'change':
531
if x not in self._on_change:
532
self._on_change[x] = []
533
self._on_change[x].append(f)
534
elif event == 'del':
535
if x not in self._on_del:
536
self._on_del[x] = []
537
self._on_del[x].append(f)
538
539
def remove(self, event, x, f):
540
if event == 'change' and x in self._on_change:
541
v = self._on_change[x]
542
i = v.find(f)
543
if i != -1:
544
del v[i]
545
if len(v) == 0:
546
del self._on_change[x]
547
elif event == 'del' and x in self._on_del:
548
v = self._on_del[x]
549
i = v.find(f)
550
if i != -1:
551
del v[i]
552
if len(v) == 0:
553
del self._on_del[x]
554
555
def __setitem__(self, x, y):
556
dict.__setitem__(self, x, y)
557
try:
558
if x in self._on_change:
559
for f in self._on_change[x]:
560
f(y)
561
if None in self._on_change:
562
for f in self._on_change[None]:
563
f(x, y)
564
except Exception as mesg:
565
print(mesg)
566
567
def __delitem__(self, x):
568
try:
569
if x in self._on_del:
570
for f in self._on_del[x]:
571
f()
572
if None in self._on_del:
573
for f in self._on_del[None]:
574
f(x)
575
except Exception as mesg:
576
print(mesg)
577
dict.__delitem__(self, x)
578
579
def set(self, x, y, do_not_trigger=None):
580
dict.__setitem__(self, x, y)
581
if x in self._on_change:
582
if do_not_trigger is None:
583
do_not_trigger = []
584
for f in self._on_change[x]:
585
if f not in do_not_trigger:
586
f(y)
587
if None in self._on_change:
588
for f in self._on_change[None]:
589
f(x, y)
590
591
592
class TemporaryURL:
593
594
def __init__(self, url, ttl):
595
self.url = url
596
self.ttl = ttl
597
598
def __repr__(self):
599
return repr(self.url)
600
601
def __str__(self):
602
return self.url
603
604
605
namespace = Namespace({})
606
607
608
class Salvus(object):
609
"""
610
Cell execution state object and wrapper for access to special CoCalc Server functionality.
611
612
An instance of this object is created each time you execute a cell. It has various methods
613
for sending different types of output messages, links to files, etc. Type 'help(smc)' for
614
more details.
615
616
OUTPUT LIMITATIONS -- There is an absolute limit on the number of messages output for a given
617
cell, and also the size of the output message for each cell. You can access or change
618
those limits dynamically in a worksheet as follows by viewing or changing any of the
619
following variables::
620
621
sage_server.MAX_STDOUT_SIZE # max length of each stdout output message
622
sage_server.MAX_STDERR_SIZE # max length of each stderr output message
623
sage_server.MAX_MD_SIZE # max length of each md (markdown) output message
624
sage_server.MAX_HTML_SIZE # max length of each html output message
625
sage_server.MAX_TEX_SIZE # max length of tex output message
626
sage_server.MAX_OUTPUT_MESSAGES # max number of messages output for a cell.
627
628
And::
629
630
sage_server.MAX_OUTPUT # max total character output for a single cell; computation
631
# terminated/truncated if sum of above exceeds this.
632
"""
633
Namespace = Namespace
634
_prefix = ''
635
_postfix = ''
636
_default_mode = 'sage'
637
_py_features = {}
638
639
def _flush_stdio(self):
640
"""
641
Flush the standard output streams. This should be called before sending any message
642
that produces output.
643
"""
644
sys.stdout.flush()
645
sys.stderr.flush()
646
647
def __repr__(self):
648
return ''
649
650
def __init__(self, conn, id, data=None, cell_id=None, message_queue=None):
651
self._conn = conn
652
self._num_output_messages = 0
653
self._total_output_length = 0
654
self._output_warning_sent = False
655
self._id = id
656
self._done = True # done=self._done when last execute message is sent; e.g., set self._done = False to not close cell on code term.
657
self.data = data
658
self.cell_id = cell_id
659
self.namespace = namespace
660
self.message_queue = message_queue
661
self.code_decorators = [] # gets reset if there are code decorators
662
# Alias: someday remove all references to "salvus" and instead use smc.
663
# For now this alias is easier to think of and use.
664
namespace['smc'] = namespace[
665
'salvus'] = self # beware of circular ref?
666
# Monkey patch in our "require" command.
667
namespace['require'] = self.require
668
# Make the salvus object itself available when doing "from sage.all import *".
669
import sage.all
670
sage.all.salvus = self
671
672
def _send_output(self, *args, **kwds):
673
if self._output_warning_sent:
674
raise KeyboardInterrupt
675
mesg = message.output(*args, **kwds)
676
if not mesg.get('once', False):
677
self._num_output_messages += 1
678
from . import sage_server
679
680
if self._num_output_messages > sage_server.MAX_OUTPUT_MESSAGES:
681
self._output_warning_sent = True
682
err = "\nToo many output messages: %s (at most %s per cell -- type 'smc?' to learn how to raise this limit): attempting to terminate..." % (
683
self._num_output_messages, sage_server.MAX_OUTPUT_MESSAGES)
684
self._conn.send_json(
685
message.output(stderr=err, id=self._id, once=False, done=True))
686
raise KeyboardInterrupt
687
688
n = self._conn.send_json(mesg)
689
self._total_output_length += n
690
691
if self._total_output_length > sage_server.MAX_OUTPUT:
692
self._output_warning_sent = True
693
err = "\nOutput too long: %s -- MAX_OUTPUT (=%s) exceeded (type 'smc?' to learn how to raise this limit): attempting to terminate..." % (
694
self._total_output_length, sage_server.MAX_OUTPUT)
695
self._conn.send_json(
696
message.output(stderr=err, id=self._id, once=False, done=True))
697
raise KeyboardInterrupt
698
699
def obj(self, obj, done=False):
700
self._send_output(obj=obj, id=self._id, done=done)
701
return self
702
703
def link(self, filename, label=None, foreground=True, cls=''):
704
"""
705
Output a clickable link to a file somewhere in this project. The filename
706
path must be relative to the current working directory of the Python process.
707
708
The simplest way to use this is
709
710
salvus.link("../name/of/file") # any relative path to any file
711
712
This creates a link, which when clicked on, opens that file in the foreground.
713
714
If the filename is the name of a directory, clicking will instead
715
open the file browser on that directory:
716
717
salvus.link("../name/of/directory") # clicking on the resulting link opens a directory
718
719
If you would like a button instead of a link, pass cls='btn'. You can use any of
720
the standard Bootstrap button classes, e.g., btn-small, btn-large, btn-success, etc.
721
722
If you would like to change the text in the link (or button) to something
723
besides the default (filename), just pass arbitrary HTML to the label= option.
724
725
INPUT:
726
727
- filename -- a relative path to a file or directory
728
- label -- (default: the filename) html label for the link
729
- foreground -- (default: True); if True, opens link in the foreground
730
- cls -- (default: '') optional CSS classes, such as 'btn'.
731
732
EXAMPLES:
733
734
Use as a line decorator::
735
736
%salvus.link name/of/file.foo
737
738
Make a button::
739
740
salvus.link("foo/bar/", label="The Bar Directory", cls='btn')
741
742
Make two big blue buttons with plots in them::
743
744
plot(sin, 0, 20).save('sin.png')
745
plot(cos, 0, 20).save('cos.png')
746
for img in ['sin.png', 'cos.png']:
747
salvus.link(img, label="<img width='150px' src='%s'>"%salvus.file(img, show=False), cls='btn btn-large btn-primary')
748
749
750
751
"""
752
path = os.path.abspath(filename)[len(os.environ['HOME']) + 1:]
753
if label is None:
754
label = filename
755
id = uuid()
756
self.html("<a class='%s' style='cursor:pointer'; id='%s'></a>" %
757
(cls, id))
758
759
s = "$('#%s').html(obj.label).click(function() {%s; return false;});" % (
760
id, self._action(path, foreground))
761
self.javascript(s,
762
obj={
763
'label': label,
764
'path': path,
765
'foreground': foreground
766
},
767
once=False)
768
769
def _action(self, path, foreground):
770
if os.path.isdir(path):
771
if foreground:
772
action = "worksheet.project_page.open_directory(obj.path);"
773
else:
774
action = "worksheet.project_page.set_current_path(obj.path);"
775
else:
776
action = "worksheet.project_page.open_file({'path':obj.path, 'foreground': obj.foreground});"
777
return action
778
779
def open_tab(self, filename, foreground=True):
780
"""
781
Open a new file (or directory) document in another tab.
782
See the documentation for salvus.link.
783
"""
784
path = os.path.abspath(filename)[len(os.environ['HOME']) + 1:]
785
self.javascript(self._action(path, foreground),
786
obj={
787
'path': path,
788
'foreground': foreground
789
},
790
once=True)
791
792
def close_tab(self, filename):
793
"""
794
Close an open file tab. The filename is relative to the current working directory.
795
"""
796
self.javascript("worksheet.project_page.close_file(obj)",
797
obj=filename,
798
once=True)
799
800
def threed(
801
self,
802
g, # sage Graphic3d object.
803
width=None,
804
height=None,
805
frame=True, # True/False or {'color':'black', 'thickness':.4, 'labels':True, 'fontsize':14, 'draw':True,
806
# 'xmin':?, 'xmax':?, 'ymin':?, 'ymax':?, 'zmin':?, 'zmax':?}
807
background=None,
808
foreground=None,
809
spin=False,
810
aspect_ratio=None,
811
frame_aspect_ratio=None, # synonym for aspect_ratio
812
done=False,
813
renderer=None, # None, 'webgl', or 'canvas'
814
):
815
816
from .graphics import graphics3d_to_jsonable, json_float as f
817
818
# process options, combining ones set explicitly above with ones inherited from 3d scene
819
opts = {
820
'width': width,
821
'height': height,
822
'background': background,
823
'foreground': foreground,
824
'spin': spin,
825
'aspect_ratio': aspect_ratio,
826
'renderer': renderer
827
}
828
829
extra_kwds = {} if g._extra_kwds is None else g._extra_kwds
830
831
# clean up and normalize aspect_ratio option
832
if aspect_ratio is None:
833
if frame_aspect_ratio is not None:
834
aspect_ratio = frame_aspect_ratio
835
elif 'frame_aspect_ratio' in extra_kwds:
836
aspect_ratio = extra_kwds['frame_aspect_ratio']
837
elif 'aspect_ratio' in extra_kwds:
838
aspect_ratio = extra_kwds['aspect_ratio']
839
if aspect_ratio is not None:
840
if aspect_ratio == 1 or aspect_ratio == "automatic":
841
aspect_ratio = None
842
elif not (isinstance(aspect_ratio,
843
(list, tuple)) and len(aspect_ratio) == 3):
844
raise TypeError(
845
"aspect_ratio must be None, 1 or a 3-tuple, but it is '%s'"
846
% (aspect_ratio, ))
847
else:
848
aspect_ratio = [f(x) for x in aspect_ratio]
849
850
opts['aspect_ratio'] = aspect_ratio
851
852
for k in [
853
'spin',
854
'height',
855
'width',
856
'background',
857
'foreground',
858
'renderer',
859
]:
860
if k in extra_kwds and not opts.get(k, None):
861
opts[k] = extra_kwds[k]
862
863
if not isinstance(opts['spin'], bool):
864
opts['spin'] = f(opts['spin'])
865
opts['width'] = f(opts['width'])
866
opts['height'] = f(opts['height'])
867
868
# determine the frame
869
b = g.bounding_box()
870
xmin, xmax, ymin, ymax, zmin, zmax = b[0][0], b[1][0], b[0][1], b[1][
871
1], b[0][2], b[1][2]
872
fr = opts['frame'] = {
873
'xmin': f(xmin),
874
'xmax': f(xmax),
875
'ymin': f(ymin),
876
'ymax': f(ymax),
877
'zmin': f(zmin),
878
'zmax': f(zmax)
879
}
880
881
if isinstance(frame, dict):
882
for k in list(fr.keys()):
883
if k in frame:
884
fr[k] = f(frame[k])
885
fr['draw'] = frame.get('draw', True)
886
fr['color'] = frame.get('color', None)
887
fr['thickness'] = f(frame.get('thickness', None))
888
fr['labels'] = frame.get('labels', None)
889
if 'fontsize' in frame:
890
fr['fontsize'] = int(frame['fontsize'])
891
elif isinstance(frame, bool):
892
fr['draw'] = frame
893
894
# convert the Sage graphics object to a JSON object that can be rendered
895
scene = {'opts': opts, 'obj': graphics3d_to_jsonable(g)}
896
897
# Store that object in the database, rather than sending it directly as an output message.
898
# We do this since obj can easily be quite large/complicated, and managing it as part of the
899
# document is too slow and doesn't scale.
900
blob = json.dumps(scene, separators=(',', ':'))
901
uuid = self._conn.send_blob(blob)
902
903
# flush output (so any text appears before 3d graphics, in case they are interleaved)
904
self._flush_stdio()
905
906
# send message pointing to the 3d 'file', which will get downloaded from database
907
self._send_output(id=self._id,
908
file={
909
'filename': unicode8("%s.sage3d" % uuid),
910
'uuid': uuid
911
},
912
done=done)
913
914
def d3_graph(self, g, **kwds):
915
from .graphics import graph_to_d3_jsonable
916
self._send_output(id=self._id,
917
d3={
918
"viewer": "graph",
919
"data": graph_to_d3_jsonable(g, **kwds)
920
})
921
922
def file(self,
923
filename,
924
show=True,
925
done=False,
926
download=False,
927
once=False,
928
events=None,
929
raw=False,
930
text=None):
931
"""
932
Display or provide a link to the given file. Raises a RuntimeError if this
933
is not possible, e.g, if the file is too large.
934
935
If show=True (the default), the browser will show the file,
936
or provide a clickable link to it if there is no way to show it.
937
If text is also given that will be used instead of the path to the file.
938
939
If show=False, this function returns an object T such that
940
T.url (or str(t)) is a string of the form "/blobs/filename?uuid=the_uuid"
941
that can be used to access the file even if the file is immediately
942
deleted after calling this function (the file is stored in a database).
943
Also, T.ttl is the time to live (in seconds) of the object. A ttl of
944
0 means the object is permanently available.
945
946
raw=False (the default):
947
If you use the URL
948
/blobs/filename?uuid=the_uuid&download
949
then the server will include a header that tells the browser to
950
download the file to disk instead of displaying it. Only relatively
951
small files can be made available this way. However, they remain
952
available (for a day) even *after* the file is deleted.
953
NOTE: It is safe to delete the file immediately after this
954
function (salvus.file) returns.
955
956
raw=True:
957
Instead, the URL is to the raw file, which is served directly
958
from the project:
959
/project-id/raw/path/to/filename
960
This will only work if the file is not deleted; however, arbitrarily
961
large files can be streamed this way. This is useful for animations.
962
963
This function creates an output message {file:...}; if the user saves
964
a worksheet containing this message, then any referenced blobs are made
965
permanent in the database.
966
967
The uuid is based on the Sha-1 hash of the file content (it is computed using the
968
function sage_server.uuidsha1). Any two files with the same content have the
969
same Sha1 hash.
970
971
The file does NOT have to be in the HOME directory.
972
"""
973
filename = unicode8(filename)
974
if raw:
975
info = self.project_info()
976
path = os.path.abspath(filename)
977
home = os.environ['HOME'] + '/'
978
979
if not path.startswith(home):
980
# Attempt to use the $HOME/.smc/root symlink instead:
981
path = os.path.join(os.environ['HOME'], '.smc', 'root',
982
path.lstrip('/'))
983
984
if path.startswith(home):
985
path = path[len(home):]
986
else:
987
raise ValueError(
988
"can only send raw files in your home directory -- path='%s'"
989
% path)
990
url = os.path.join('/', info['base_url'].strip('/'),
991
info['project_id'], 'raw', path.lstrip('/'))
992
if show:
993
self._flush_stdio()
994
self._send_output(id=self._id,
995
once=once,
996
file={
997
'filename': filename,
998
'url': url,
999
'show': show,
1000
'text': text
1001
},
1002
events=events,
1003
done=done)
1004
return
1005
else:
1006
return TemporaryURL(url=url, ttl=0)
1007
1008
file_uuid = self._conn.send_file(filename)
1009
1010
mesg = None
1011
while mesg is None:
1012
self.message_queue.recv()
1013
for i, (typ, m) in enumerate(self.message_queue.queue):
1014
if typ == 'json' and m.get('event') == 'save_blob' and m.get(
1015
'sha1') == file_uuid:
1016
mesg = m
1017
del self.message_queue[i]
1018
break
1019
1020
if 'error' in mesg:
1021
raise RuntimeError("error saving blob -- %s" % mesg['error'])
1022
1023
self._flush_stdio()
1024
self._send_output(id=self._id,
1025
once=once,
1026
file={
1027
'filename': filename,
1028
'uuid': file_uuid,
1029
'show': show,
1030
'text': text
1031
},
1032
events=events,
1033
done=done)
1034
if not show:
1035
info = self.project_info()
1036
url = "%s/blobs/%s?uuid=%s" % (info['base_url'], filename,
1037
file_uuid)
1038
if download:
1039
url += '?download'
1040
return TemporaryURL(url=url, ttl=mesg.get('ttl', 0))
1041
1042
def python_future_feature(self, feature=None, enable=None):
1043
"""
1044
Allow users to enable, disable, and query the features in the python __future__ module.
1045
"""
1046
if feature is None:
1047
if enable is not None:
1048
raise ValueError(
1049
"enable may not be specified when feature is None")
1050
return sorted(Salvus._py_features.keys())
1051
1052
attr = getattr(future, feature, None)
1053
if (feature not in future.all_feature_names) or (
1054
attr is None) or not isinstance(attr, future._Feature):
1055
raise RuntimeError("future feature %.50r is not defined" %
1056
(feature, ))
1057
1058
if enable is None:
1059
return feature in Salvus._py_features
1060
1061
if enable:
1062
Salvus._py_features[feature] = attr
1063
else:
1064
try:
1065
del Salvus._py_features[feature]
1066
except KeyError:
1067
pass
1068
1069
def default_mode(self, mode=None):
1070
"""
1071
Set the default mode for cell evaluation. This is equivalent
1072
to putting %mode at the top of any cell that does not start
1073
with %. Use salvus.default_mode() to return the current mode.
1074
Use salvus.default_mode("") to have no default mode.
1075
1076
This is implemented using salvus.cell_prefix.
1077
"""
1078
if mode is None:
1079
return Salvus._default_mode
1080
Salvus._default_mode = mode
1081
if mode == "sage":
1082
self.cell_prefix("")
1083
else:
1084
self.cell_prefix("%" + mode)
1085
1086
def cell_prefix(self, prefix=None):
1087
"""
1088
Make it so that the given prefix code is textually
1089
prepending to the input before evaluating any cell, unless
1090
the first character of the cell is a %.
1091
1092
To append code at the end, use cell_postfix.
1093
1094
INPUT:
1095
1096
- ``prefix`` -- None (to return prefix) or a string ("" to disable)
1097
1098
EXAMPLES:
1099
1100
Make it so every cell is timed:
1101
1102
salvus.cell_prefix('%time')
1103
1104
Make it so cells are typeset using latex, and latex comments are allowed even
1105
as the first line.
1106
1107
salvus.cell_prefix('%latex')
1108
1109
%sage salvus.cell_prefix('')
1110
1111
Evaluate each cell using GP (Pari) and display the time it took:
1112
1113
salvus.cell_prefix('%time\n%gp')
1114
1115
%sage salvus.cell_prefix('') # back to normal
1116
"""
1117
if prefix is None:
1118
return Salvus._prefix
1119
else:
1120
Salvus._prefix = prefix
1121
1122
def cell_postfix(self, postfix=None):
1123
"""
1124
Make it so that the given code is textually
1125
appended to the input before evaluating a cell.
1126
To prepend code at the beginning, use cell_prefix.
1127
1128
INPUT:
1129
1130
- ``postfix`` -- None (to return postfix) or a string ("" to disable)
1131
1132
EXAMPLES:
1133
1134
Print memory usage after evaluating each cell:
1135
1136
salvus.cell_postfix('print("%s MB used"%int(get_memory_usage()))')
1137
1138
Return to normal
1139
1140
salvus.set_cell_postfix('')
1141
1142
"""
1143
if postfix is None:
1144
return Salvus._postfix
1145
else:
1146
Salvus._postfix = postfix
1147
1148
def execute(self, code, namespace=None, preparse=True, locals=None):
1149
1150
ascii_warn = False
1151
code_error = False
1152
if sys.getdefaultencoding() == 'ascii':
1153
for c in code:
1154
if ord(c) >= 128:
1155
ascii_warn = True
1156
break
1157
1158
if namespace is None:
1159
namespace = self.namespace
1160
1161
# clear pylab figure (takes a few microseconds)
1162
if pylab is not None:
1163
pylab.clf()
1164
1165
compile_flags = reduce(operator.or_,
1166
(feature.compiler_flag
1167
for feature in Salvus._py_features.values()),
1168
0)
1169
1170
#code = sage_parsing.strip_leading_prompts(code) # broken -- wrong on "def foo(x):\n print(x)"
1171
blocks = sage_parsing.divide_into_blocks(code)
1172
1173
try:
1174
import sage.repl
1175
# CRITICAL -- we do NOT import sage.repl.interpreter!!!!!!!
1176
# That would waste several seconds importing ipython and much more, which is just dumb.
1177
# The only reason this is needed below is if the user has run preparser(False), which
1178
# would cause sage.repl.interpreter to be imported at that point (as preparser is
1179
# lazy imported.)
1180
sage_repl_interpreter = sage.repl.interpreter
1181
except:
1182
pass # expected behavior usually, since sage.repl.interpreter usually not imported (only used by command line...)
1183
1184
import sage.misc.session
1185
for start, stop, block in blocks:
1186
# if import sage.repl.interpreter fails, sag_repl_interpreter is unreferenced
1187
try:
1188
do_pp = getattr(sage_repl_interpreter, '_do_preparse', True)
1189
except:
1190
do_pp = True
1191
if preparse and do_pp:
1192
block = sage_parsing.preparse_code(block)
1193
sys.stdout.reset()
1194
sys.stderr.reset()
1195
try:
1196
b = block.rstrip()
1197
# get rid of comments at the end of the line -- issue #1835
1198
#from ushlex import shlex
1199
#s = shlex(b)
1200
#s.commenters = '#'
1201
#s.quotes = '"\''
1202
#b = ''.join(s)
1203
# e.g. now a line like 'x = test? # bar' becomes 'x=test?'
1204
if b.endswith('??'):
1205
p = sage_parsing.introspect(b,
1206
namespace=namespace,
1207
preparse=False)
1208
self.code(source=p['result'], mode="python")
1209
elif b.endswith('?'):
1210
p = sage_parsing.introspect(b,
1211
namespace=namespace,
1212
preparse=False)
1213
self.code(source=p['result'], mode="text/x-rst")
1214
else:
1215
reload_attached_files_if_mod_smc()
1216
if execute.count < 2:
1217
execute.count += 1
1218
if execute.count == 2:
1219
# this fixup has to happen after first block has executed (os.chdir etc)
1220
# but before user assigns any variable in worksheet
1221
# sage.misc.session.init() is not called until first call of show_identifiers
1222
# BUGFIX: be careful to *NOT* assign to _!! see https://github.com/sagemathinc/cocalc/issues/1107
1223
block2 = "sage.misc.session.state_at_init = dict(globals());sage.misc.session._dummy=sage.misc.session.show_identifiers();\n"
1224
exec(compile(block2, '', 'single'), namespace,
1225
locals)
1226
b2a = """
1227
if 'SAGE_STARTUP_FILE' in os.environ and os.path.isfile(os.environ['SAGE_STARTUP_FILE']):
1228
try:
1229
load(os.environ['SAGE_STARTUP_FILE'])
1230
except:
1231
sys.stdout.flush()
1232
sys.stderr.write('\\nException loading startup file: {}\\n'.format(os.environ['SAGE_STARTUP_FILE']))
1233
sys.stderr.flush()
1234
raise
1235
"""
1236
exec(compile(b2a, '', 'exec'), namespace, locals)
1237
features = sage_parsing.get_future_features(
1238
block, 'single')
1239
if features:
1240
compile_flags = reduce(
1241
operator.or_, (feature.compiler_flag
1242
for feature in features.values()),
1243
compile_flags)
1244
exec(
1245
compile(block + '\n',
1246
'',
1247
'single',
1248
flags=compile_flags), namespace, locals)
1249
if features:
1250
Salvus._py_features.update(features)
1251
sys.stdout.flush()
1252
sys.stderr.flush()
1253
except:
1254
if ascii_warn:
1255
sys.stderr.write(
1256
'\n\n*** WARNING: Code contains non-ascii characters ***\n'
1257
)
1258
for c in '\u201c\u201d':
1259
if c in code:
1260
sys.stderr.write(
1261
'*** Maybe the character < %s > should be replaced by < " > ? ***\n'
1262
% c)
1263
break
1264
sys.stderr.write('\n\n')
1265
1266
if six.PY2:
1267
from exceptions import SyntaxError, TypeError
1268
# py3: all standard errors are available by default via "builtin", not available here for some reason ...
1269
if six.PY3:
1270
from builtins import SyntaxError, TypeError
1271
1272
exc_type, _, _ = sys.exc_info()
1273
if exc_type in [SyntaxError, TypeError]:
1274
from .sage_parsing import strip_string_literals
1275
code0, _, _ = strip_string_literals(code)
1276
implicit_mul = RE_POSSIBLE_IMPLICIT_MUL.findall(code0)
1277
if len(implicit_mul) > 0:
1278
implicit_mul_list = ', '.join(
1279
str(_) for _ in implicit_mul)
1280
# we know there is a SyntaxError and there could be an implicit multiplication
1281
sys.stderr.write(
1282
'\n\n*** WARNING: Code contains possible implicit multiplication ***\n'
1283
)
1284
sys.stderr.write(
1285
'*** Check if any of [ %s ] need a "*" sign for multiplication, e.g. 5x should be 5*x ! ***\n\n'
1286
% implicit_mul_list)
1287
1288
sys.stdout.flush()
1289
sys.stderr.write('Error in lines %s-%s\n' %
1290
(start + 1, stop + 1))
1291
traceback.print_exc()
1292
sys.stderr.flush()
1293
break
1294
1295
def execute_with_code_decorators(self,
1296
code_decorators,
1297
code,
1298
preparse=True,
1299
namespace=None,
1300
locals=None):
1301
"""
1302
salvus.execute_with_code_decorators is used when evaluating
1303
code blocks that are set to any non-default code_decorator.
1304
"""
1305
import sage # used below as a code decorator
1306
if is_string(code_decorators):
1307
code_decorators = [code_decorators]
1308
1309
if preparse:
1310
code_decorators = list(
1311
map(sage_parsing.preparse_code, code_decorators))
1312
1313
code_decorators = [
1314
eval(code_decorator, self.namespace)
1315
for code_decorator in code_decorators
1316
]
1317
1318
# The code itself may want to know exactly what code decorators are in effect.
1319
# For example, r.eval can do extra things when being used as a decorator.
1320
self.code_decorators = code_decorators
1321
1322
for i, code_decorator in enumerate(code_decorators):
1323
# eval is for backward compatibility
1324
if not hasattr(code_decorator, 'eval') and hasattr(
1325
code_decorator, 'before'):
1326
code_decorators[i] = code_decorator.before(code)
1327
1328
for code_decorator in reversed(code_decorators):
1329
# eval is for backward compatibility
1330
if hasattr(code_decorator, 'eval'):
1331
print(code_decorator.eval(
1332
code, locals=self.namespace)) # removed , end=' '
1333
code = ''
1334
elif code_decorator is sage:
1335
# special case -- the sage module (i.e., %sage) should do nothing.
1336
pass
1337
else:
1338
code = code_decorator(code)
1339
if code is None:
1340
code = ''
1341
1342
if code != '' and is_string(code):
1343
self.execute(code,
1344
preparse=preparse,
1345
namespace=namespace,
1346
locals=locals)
1347
1348
for code_decorator in code_decorators:
1349
if not hasattr(code_decorator, 'eval') and hasattr(
1350
code_decorator, 'after'):
1351
code_decorator.after(code)
1352
1353
def html(self, html, done=False, once=None):
1354
"""
1355
Display html in the output stream.
1356
1357
EXAMPLE:
1358
1359
salvus.html("<b>Hi</b>")
1360
"""
1361
self._flush_stdio()
1362
self._send_output(html=unicode8(html),
1363
id=self._id,
1364
done=done,
1365
once=once)
1366
1367
def md(self, md, done=False, once=None):
1368
"""
1369
Display markdown in the output stream.
1370
1371
EXAMPLE:
1372
1373
salvus.md("**Hi**")
1374
"""
1375
self._flush_stdio()
1376
self._send_output(md=unicode8(md), id=self._id, done=done, once=once)
1377
1378
def pdf(self, filename, **kwds):
1379
sage_salvus.show_pdf(filename, **kwds)
1380
1381
def tex(self, obj, display=False, done=False, once=None, **kwds):
1382
"""
1383
Display obj nicely using TeX rendering.
1384
1385
INPUT:
1386
1387
- obj -- latex string or object that is automatically be converted to TeX
1388
- display -- (default: False); if True, typeset as display math (so centered, etc.)
1389
"""
1390
self._flush_stdio()
1391
tex = obj if is_string(obj) else self.namespace['latex'](obj, **kwds)
1392
self._send_output(tex={
1393
'tex': tex,
1394
'display': display
1395
},
1396
id=self._id,
1397
done=done,
1398
once=once)
1399
return self
1400
1401
def start_executing(self):
1402
self._send_output(done=False, id=self._id)
1403
1404
def clear(self, done=False):
1405
self._send_output(clear=True, id=self._id, done=done)
1406
1407
def delete_last_output(self, done=False):
1408
self._send_output(delete_last=True, id=self._id, done=done)
1409
1410
def stdout(self, output, done=False, once=None):
1411
"""
1412
Send the string output (or unicode8(output) if output is not a
1413
string) to the standard output stream of the compute cell.
1414
1415
INPUT:
1416
1417
- output -- string or object
1418
1419
"""
1420
stdout = output if is_string(output) else unicode8(output)
1421
self._send_output(stdout=stdout, done=done, id=self._id, once=once)
1422
return self
1423
1424
def stderr(self, output, done=False, once=None):
1425
"""
1426
Send the string output (or unicode8(output) if output is not a
1427
string) to the standard error stream of the compute cell.
1428
1429
INPUT:
1430
1431
- output -- string or object
1432
1433
"""
1434
stderr = output if is_string(output) else unicode8(output)
1435
self._send_output(stderr=stderr, done=done, id=self._id, once=once)
1436
return self
1437
1438
def code(
1439
self,
1440
source, # actual source code
1441
mode=None, # the syntax highlight codemirror mode
1442
filename=None, # path of file it is contained in (if applicable)
1443
lineno=-1, # line number where source starts (0-based)
1444
done=False,
1445
once=None):
1446
"""
1447
Send a code message, which is to be rendered as code by the client, with
1448
appropriate syntax highlighting, maybe a link to open the source file, etc.
1449
"""
1450
source = source if is_string(source) else unicode8(source)
1451
code = {
1452
'source': source,
1453
'filename': filename,
1454
'lineno': int(lineno),
1455
'mode': mode
1456
}
1457
self._send_output(code=code, done=done, id=self._id, once=once)
1458
return self
1459
1460
def _execute_interact(self, id, vals):
1461
if id not in sage_salvus.interacts:
1462
print("(Evaluate this cell to use this interact.)")
1463
#raise RuntimeError("Error: No interact with id %s"%id)
1464
else:
1465
sage_salvus.interacts[id](vals)
1466
1467
def interact(self, f, done=False, once=None, **kwds):
1468
I = sage_salvus.InteractCell(f, **kwds)
1469
self._flush_stdio()
1470
self._send_output(interact=I.jsonable(),
1471
id=self._id,
1472
done=done,
1473
once=once)
1474
return sage_salvus.InteractFunction(I)
1475
1476
def javascript(self,
1477
code,
1478
once=False,
1479
coffeescript=False,
1480
done=False,
1481
obj=None):
1482
"""
1483
Execute the given Javascript code as part of the output
1484
stream. This same code will be executed (at exactly this
1485
point in the output stream) every time the worksheet is
1486
rendered.
1487
1488
See the docs for the top-level javascript function for more details.
1489
1490
INPUT:
1491
1492
- code -- a string
1493
- once -- boolean (default: FAlse); if True the Javascript is
1494
only executed once, not every time the cell is loaded. This
1495
is what you would use if you call salvus.stdout, etc. Use
1496
once=False, e.g., if you are using javascript to make a DOM
1497
element draggable (say). WARNING: If once=True, then the
1498
javascript is likely to get executed before other output to
1499
a given cell is even rendered.
1500
- coffeescript -- boolean (default: False); if True, the input
1501
code is first converted from CoffeeScript to Javascript.
1502
1503
At least the following Javascript objects are defined in the
1504
scope in which the code is evaluated::
1505
1506
- cell -- jQuery wrapper around the current compute cell
1507
- salvus.stdout, salvus.stderr, salvus.html, salvus.tex -- all
1508
allow you to write additional output to the cell
1509
- worksheet - jQuery wrapper around the current worksheet DOM object
1510
- obj -- the optional obj argument, which is passed via JSON serialization
1511
"""
1512
if obj is None:
1513
obj = {}
1514
self._send_output(javascript={
1515
'code': code,
1516
'coffeescript': coffeescript
1517
},
1518
id=self._id,
1519
done=done,
1520
obj=obj,
1521
once=once)
1522
1523
def coffeescript(self, *args, **kwds):
1524
"""
1525
This is the same as salvus.javascript, but with coffeescript=True.
1526
1527
See the docs for the top-level javascript function for more details.
1528
"""
1529
kwds['coffeescript'] = True
1530
self.javascript(*args, **kwds)
1531
1532
def raw_input(self,
1533
prompt='',
1534
default='',
1535
placeholder='',
1536
input_width=None,
1537
label_width=None,
1538
done=False,
1539
type=None): # done is ignored here
1540
self._flush_stdio()
1541
m = {'prompt': unicode8(prompt)}
1542
if input_width is not None:
1543
m['input_width'] = unicode8(input_width)
1544
if label_width is not None:
1545
m['label_width'] = unicode8(label_width)
1546
if default:
1547
m['value'] = unicode8(default)
1548
if placeholder:
1549
m['placeholder'] = unicode8(placeholder)
1550
self._send_output(raw_input=m, id=self._id)
1551
typ, mesg = self.message_queue.next_mesg()
1552
log("handling raw input message ", truncate_text(unicode8(mesg), 400))
1553
if typ == 'json' and mesg['event'] == 'sage_raw_input':
1554
# everything worked out perfectly
1555
self.delete_last_output()
1556
m['value'] = mesg['value'] # as unicode!
1557
m['submitted'] = True
1558
self._send_output(raw_input=m, id=self._id)
1559
value = mesg['value']
1560
if type is not None:
1561
if type == 'sage':
1562
value = sage_salvus.sage_eval(value)
1563
else:
1564
try:
1565
value = type(value)
1566
except TypeError:
1567
# Some things in Sage are clueless about unicode for some reason...
1568
# Let's at least try, in case the unicode can convert to a string.
1569
value = type(str(value))
1570
return value
1571
else:
1572
raise KeyboardInterrupt(
1573
"raw_input interrupted by another action: event='%s' (expected 'sage_raw_input')"
1574
% mesg['event'])
1575
1576
def _check_component(self, component):
1577
if component not in ['input', 'output']:
1578
raise ValueError("component must be 'input' or 'output'")
1579
1580
def hide(self, component):
1581
"""
1582
Hide the given component ('input' or 'output') of the cell.
1583
"""
1584
self._check_component(component)
1585
self._send_output(self._id, hide=component)
1586
1587
def show(self, component):
1588
"""
1589
Show the given component ('input' or 'output') of the cell.
1590
"""
1591
self._check_component(component)
1592
self._send_output(self._id, show=component)
1593
1594
def notify(self, **kwds):
1595
"""
1596
Display a graphical notification using the alert_message Javascript function.
1597
1598
INPUTS:
1599
1600
- `type: "default"` - Type of the notice. "default", "warning", "info", "success", or "error".
1601
- `title: ""` - The notice's title.
1602
- `message: ""` - The notice's text.
1603
- `timeout: ?` - Delay in seconds before the notice is automatically removed.
1604
1605
EXAMPLE:
1606
1607
salvus.notify(type="warning", title="This warning", message="This is a quick message.", timeout=3)
1608
"""
1609
obj = {}
1610
for k, v in kwds.items():
1611
if k == 'text': # backward compat
1612
k = 'message'
1613
elif k == 'type' and v == 'notice': # backward compat
1614
v = 'default'
1615
obj[k] = sage_salvus.jsonable(v)
1616
if k == 'delay': # backward compat
1617
obj['timeout'] = v / 1000.0 # units are in seconds now.
1618
1619
self.javascript("alert_message(obj)", once=True, obj=obj)
1620
1621
def execute_javascript(self, code, coffeescript=False, obj=None):
1622
"""
1623
Tell the browser to execute javascript. Basically the same as
1624
salvus.javascript with once=True (the default), except this
1625
isn't tied to a particular cell. There is a worksheet object
1626
defined in the scope of the evaluation.
1627
1628
See the docs for the top-level javascript function for more details.
1629
"""
1630
self._conn.send_json(
1631
message.execute_javascript(code,
1632
coffeescript=coffeescript,
1633
obj=json.dumps(obj,
1634
separators=(',', ':'))))
1635
1636
def execute_coffeescript(self, *args, **kwds):
1637
"""
1638
This is the same as salvus.execute_javascript, but with coffeescript=True.
1639
1640
See the docs for the top-level javascript function for more details.
1641
"""
1642
kwds['coffeescript'] = True
1643
self.execute_javascript(*args, **kwds)
1644
1645
def _cython(self, filename, **opts):
1646
"""
1647
Return module obtained by compiling the Cython code in the
1648
given file.
1649
1650
INPUT:
1651
1652
- filename -- name of a Cython file
1653
- all other options are passed to sage.misc.cython.cython unchanged,
1654
except for use_cache which defaults to True (instead of False)
1655
1656
OUTPUT:
1657
1658
- a module
1659
"""
1660
if 'use_cache' not in opts:
1661
opts['use_cache'] = True
1662
import sage.misc.cython
1663
modname, path = sage.misc.cython.cython(filename, **opts)
1664
try:
1665
sys.path.insert(0, path)
1666
module = __import__(modname)
1667
finally:
1668
del sys.path[0]
1669
return module
1670
1671
def _import_code(self, content, **opts):
1672
while True:
1673
py_file_base = uuid().replace('-', '_')
1674
if not os.path.exists(py_file_base + '.py'):
1675
break
1676
try:
1677
open(py_file_base + '.py', 'w').write(content)
1678
try:
1679
sys.path.insert(0, os.path.abspath('.'))
1680
mod = __import__(py_file_base)
1681
finally:
1682
del sys.path[0]
1683
finally:
1684
os.unlink(py_file_base + '.py')
1685
os.unlink(py_file_base + '.pyc')
1686
return mod
1687
1688
def _sage(self, filename, **opts):
1689
import sage.misc.preparser
1690
content = "from sage.all import *\n" + sage.misc.preparser.preparse_file(
1691
open(filename).read())
1692
return self._import_code(content, **opts)
1693
1694
def _spy(self, filename, **opts):
1695
import sage.misc.preparser
1696
content = "from sage.all import Integer, RealNumber, PolynomialRing\n" + sage.misc.preparser.preparse_file(
1697
open(filename).read())
1698
return self._import_code(content, **opts)
1699
1700
def _py(self, filename, **opts):
1701
return __import__(filename)
1702
1703
def require(self, filename, **opts):
1704
if not os.path.exists(filename):
1705
raise ValueError("file '%s' must exist" % filename)
1706
base, ext = os.path.splitext(filename)
1707
if ext == '.pyx' or ext == '.spyx':
1708
return self._cython(filename, **opts)
1709
if ext == ".sage":
1710
return self._sage(filename, **opts)
1711
if ext == ".spy":
1712
return self._spy(filename, **opts)
1713
if ext == ".py":
1714
return self._py(filename, **opts)
1715
raise NotImplementedError("require file of type %s not implemented" %
1716
ext)
1717
1718
def typeset_mode(self, on=True):
1719
sage_salvus.typeset_mode(on)
1720
1721
def project_info(self):
1722
"""
1723
Return a dictionary with information about the project in which this code is running.
1724
1725
EXAMPLES::
1726
1727
sage: salvus.project_info()
1728
{"stdout":"{u'project_id': u'...', u'location': {u'username': u'teaAuZ9M', u'path': u'.', u'host': u'localhost', u'port': 22}, u'base_url': u'/...'}\n"}
1729
"""
1730
return INFO
1731
1732
1733
if six.PY2:
1734
Salvus.pdf.__func__.__doc__ = sage_salvus.show_pdf.__doc__
1735
Salvus.raw_input.__func__.__doc__ = sage_salvus.raw_input.__doc__
1736
Salvus.clear.__func__.__doc__ = sage_salvus.clear.__doc__
1737
Salvus.delete_last_output.__func__.__doc__ = sage_salvus.delete_last_output.__doc__
1738
else:
1739
Salvus.pdf.__doc__ = sage_salvus.show_pdf.__doc__
1740
Salvus.raw_input.__doc__ = sage_salvus.raw_input.__doc__
1741
Salvus.clear.__doc__ = sage_salvus.clear.__doc__
1742
Salvus.delete_last_output.__doc__ = sage_salvus.delete_last_output.__doc__
1743
1744
1745
def execute(conn, id, code, data, cell_id, preparse, message_queue):
1746
1747
salvus = Salvus(conn=conn,
1748
id=id,
1749
data=data,
1750
message_queue=message_queue,
1751
cell_id=cell_id)
1752
1753
#salvus.start_executing() # with our new mainly client-side execution this isn't needed; not doing this makes evaluation roundtrip around 100ms instead of 200ms too, which is a major win.
1754
1755
try:
1756
# initialize the salvus output streams
1757
streams = (sys.stdout, sys.stderr)
1758
sys.stdout = BufferedOutputStream(salvus.stdout)
1759
sys.stderr = BufferedOutputStream(salvus.stderr)
1760
try:
1761
# initialize more salvus functionality
1762
sage_salvus.set_salvus(salvus)
1763
namespace['sage_salvus'] = sage_salvus
1764
except:
1765
traceback.print_exc()
1766
1767
if salvus._prefix:
1768
if not code.startswith("%"):
1769
code = salvus._prefix + '\n' + code
1770
1771
if salvus._postfix:
1772
code += '\n' + salvus._postfix
1773
1774
salvus.execute(code, namespace=namespace, preparse=preparse)
1775
1776
finally:
1777
# there must be exactly one done message, unless salvus._done is False.
1778
if sys.stderr._buf:
1779
if sys.stdout._buf:
1780
sys.stdout.flush()
1781
sys.stderr.flush(done=salvus._done)
1782
else:
1783
sys.stdout.flush(done=salvus._done)
1784
(sys.stdout, sys.stderr) = streams
1785
1786
1787
# execute.count goes from 0 to 2
1788
# used for show_identifiers()
1789
execute.count = 0
1790
1791
1792
def drop_privileges(id, home, transient, username):
1793
gid = id
1794
uid = id
1795
if transient:
1796
os.chown(home, uid, gid)
1797
os.setgid(gid)
1798
os.setuid(uid)
1799
os.environ['DOT_SAGE'] = home
1800
mpl = os.environ['MPLCONFIGDIR']
1801
os.environ['MPLCONFIGDIR'] = home + mpl[5:]
1802
os.environ['HOME'] = home
1803
os.environ['IPYTHON_DIR'] = home
1804
os.environ['USERNAME'] = username
1805
os.environ['USER'] = username
1806
os.chdir(home)
1807
1808
# Monkey patch the Sage library and anything else that does not
1809
# deal well with changing user. This sucks, but it is work that
1810
# simply must be done because we're not importing the library from
1811
# scratch (which would take a long time).
1812
import sage.misc.misc
1813
sage.misc.misc.DOT_SAGE = home + '/.sage/'
1814
1815
1816
class MessageQueue(list):
1817
1818
def __init__(self, conn):
1819
self.queue = []
1820
self.conn = conn
1821
1822
def __repr__(self):
1823
return "Sage Server Message Queue"
1824
1825
def __getitem__(self, i):
1826
return self.queue[i]
1827
1828
def __delitem__(self, i):
1829
del self.queue[i]
1830
1831
def next_mesg(self):
1832
"""
1833
Remove oldest message from the queue and return it.
1834
If the queue is empty, wait for a message to arrive
1835
and return it (does not place it in the queue).
1836
"""
1837
if self.queue:
1838
return self.queue.pop()
1839
else:
1840
return self.conn.recv()
1841
1842
def recv(self):
1843
"""
1844
Wait until one message is received and enqueue it.
1845
Also returns the mesg.
1846
"""
1847
mesg = self.conn.recv()
1848
self.queue.insert(0, mesg)
1849
return mesg
1850
1851
1852
def session(conn):
1853
"""
1854
This is run by the child process that is forked off on each new
1855
connection. It drops privileges, then handles the complete
1856
compute session.
1857
1858
INPUT:
1859
1860
- ``conn`` -- the TCP connection
1861
"""
1862
mq = MessageQueue(conn)
1863
1864
pid = os.getpid()
1865
1866
# seed the random number generator(s)
1867
import sage.all
1868
sage.all.set_random_seed()
1869
import random
1870
random.seed(sage.all.initial_seed())
1871
1872
cnt = 0
1873
while True:
1874
try:
1875
typ, mesg = mq.next_mesg()
1876
1877
#print('INFO:child%s: received message "%s"'%(pid, mesg))
1878
log("handling message ", truncate_text(unicode8(mesg), 400))
1879
event = mesg['event']
1880
if event == 'terminate_session':
1881
return
1882
elif event == 'execute_code':
1883
try:
1884
execute(conn=conn,
1885
id=mesg['id'],
1886
code=mesg['code'],
1887
data=mesg.get('data', None),
1888
cell_id=mesg.get('cell_id', None),
1889
preparse=mesg.get('preparse', True),
1890
message_queue=mq)
1891
except Exception as err:
1892
log("ERROR -- exception raised '%s' when executing '%s'" %
1893
(err, mesg['code']))
1894
elif event == 'introspect':
1895
try:
1896
# check for introspect from jupyter cell
1897
prefix = Salvus._default_mode
1898
if 'top' in mesg:
1899
top = mesg['top']
1900
log('introspect cell top line %s' % top)
1901
if top.startswith("%"):
1902
prefix = top[1:]
1903
try:
1904
# see if prefix is the name of a jupyter kernel function
1905
kc = eval(prefix + "(get_kernel_client=True)",
1906
namespace, locals())
1907
kn = eval(prefix + "(get_kernel_name=True)", namespace,
1908
locals())
1909
log("jupyter introspect prefix %s kernel %s" %
1910
(prefix, kn)) # e.g. "p2", "python2"
1911
jupyter_introspect(conn=conn,
1912
id=mesg['id'],
1913
line=mesg['line'],
1914
preparse=mesg.get('preparse', True),
1915
kc=kc)
1916
except:
1917
import traceback
1918
exc_type, exc_value, exc_traceback = sys.exc_info()
1919
lines = traceback.format_exception(
1920
exc_type, exc_value, exc_traceback)
1921
log(lines)
1922
introspect(conn=conn,
1923
id=mesg['id'],
1924
line=mesg['line'],
1925
preparse=mesg.get('preparse', True))
1926
except:
1927
pass
1928
else:
1929
raise RuntimeError("invalid message '%s'" % mesg)
1930
except:
1931
# When hub connection dies, loop goes crazy.
1932
# Unfortunately, just catching SIGINT doesn't seem to
1933
# work, and leads to random exits during a
1934
# session. Howeer, when connection dies, 10000 iterations
1935
# happen almost instantly. Ugly, but it works.
1936
cnt += 1
1937
if cnt > 10000:
1938
sys.exit(0)
1939
else:
1940
pass
1941
1942
1943
def jupyter_introspect(conn, id, line, preparse, kc):
1944
import jupyter_client
1945
from queue import Empty
1946
1947
try:
1948
salvus = Salvus(conn=conn, id=id)
1949
msg_id = kc.complete(line)
1950
shell = kc.shell_channel
1951
iopub = kc.iopub_channel
1952
1953
# handle iopub responses
1954
while True:
1955
try:
1956
msg = iopub.get_msg(timeout=1)
1957
msg_type = msg['msg_type']
1958
content = msg['content']
1959
1960
except Empty:
1961
# shouldn't happen
1962
log("jupyter iopub channel empty")
1963
break
1964
1965
if msg['parent_header'].get('msg_id') != msg_id:
1966
continue
1967
1968
log("jupyter iopub recv %s %s" % (msg_type, str(content)))
1969
1970
if msg_type == 'status' and content['execution_state'] == 'idle':
1971
break
1972
1973
# handle shell responses
1974
while True:
1975
try:
1976
msg = shell.get_msg(timeout=10)
1977
msg_type = msg['msg_type']
1978
content = msg['content']
1979
1980
except:
1981
# shouldn't happen
1982
log("jupyter shell channel empty")
1983
break
1984
1985
if msg['parent_header'].get('msg_id') != msg_id:
1986
continue
1987
1988
log("jupyter shell recv %s %s" % (msg_type, str(content)))
1989
1990
if msg_type == 'complete_reply' and content['status'] == 'ok':
1991
# jupyter kernel returns matches like "xyz.append" and smc wants just "append"
1992
matches = content['matches']
1993
offset = content['cursor_end'] - content['cursor_start']
1994
completions = [s[offset:] for s in matches]
1995
mesg = message.introspect_completions(id=id,
1996
completions=completions,
1997
target=line[-offset:])
1998
conn.send_json(mesg)
1999
break
2000
except:
2001
log("jupyter completion exception: %s" % sys.exc_info()[0])
2002
2003
2004
def introspect(conn, id, line, preparse):
2005
salvus = Salvus(
2006
conn=conn, id=id
2007
) # so salvus.[tab] works -- note that Salvus(...) modifies namespace.
2008
z = sage_parsing.introspect(line, namespace=namespace, preparse=preparse)
2009
if z['get_completions']:
2010
mesg = message.introspect_completions(id=id,
2011
completions=z['result'],
2012
target=z['target'])
2013
elif z['get_help']:
2014
mesg = message.introspect_docstring(id=id,
2015
docstring=z['result'],
2016
target=z['expr'])
2017
elif z['get_source']:
2018
mesg = message.introspect_source_code(id=id,
2019
source_code=z['result'],
2020
target=z['expr'])
2021
conn.send_json(mesg)
2022
2023
2024
def handle_session_term(signum, frame):
2025
while True:
2026
try:
2027
pid, exit_status = os.waitpid(-1, os.WNOHANG)
2028
except:
2029
return
2030
if not pid: return
2031
2032
2033
secret_token = None
2034
2035
if 'COCALC_SECRET_TOKEN' in os.environ:
2036
secret_token_path = os.environ['COCALC_SECRET_TOKEN']
2037
else:
2038
secret_token_path = os.path.join(os.environ['SMC'], 'secret_token')
2039
2040
2041
def unlock_conn(conn):
2042
global secret_token
2043
if secret_token is None:
2044
try:
2045
secret_token = open(secret_token_path).read().strip()
2046
except:
2047
conn.send(six.b('n'))
2048
conn.send(
2049
six.
2050
b("Unable to accept connection, since Sage server doesn't yet know the secret token; unable to read from '%s'"
2051
% secret_token_path))
2052
conn.close()
2053
2054
n = len(secret_token)
2055
token = six.b('')
2056
while len(token) < n:
2057
token += conn.recv(n)
2058
if token != secret_token[:len(token)]:
2059
break # definitely not right -- don't try anymore
2060
if token != six.b(secret_token):
2061
log("token='%s'; secret_token='%s'" % (token, secret_token))
2062
conn.send(six.b('n')) # no -- invalid login
2063
conn.send(six.b("Invalid secret token."))
2064
conn.close()
2065
return False
2066
else:
2067
conn.send(six.b('y')) # yes -- valid login
2068
return True
2069
2070
2071
def serve_connection(conn):
2072
global PID
2073
PID = os.getpid()
2074
# First the client *must* send the secret shared token. If they
2075
# don't, we return (and the connection will have been destroyed by
2076
# unlock_conn).
2077
log("Serving a connection")
2078
log("Waiting for client to unlock the connection...")
2079
# TODO -- put in a timeout (?)
2080
if not unlock_conn(conn):
2081
log("Client failed to unlock connection. Dumping them.")
2082
return
2083
log("Connection unlocked.")
2084
2085
try:
2086
conn = ConnectionJSON(conn)
2087
typ, mesg = conn.recv()
2088
log("Received message %s" % mesg)
2089
except Exception as err:
2090
log("Error receiving message: %s (connection terminated)" % str(err))
2091
raise
2092
2093
if mesg['event'] == 'send_signal':
2094
if mesg['pid'] == 0:
2095
log("invalid signal mesg (pid=0)")
2096
else:
2097
log("Sending a signal")
2098
os.kill(mesg['pid'], mesg['signal'])
2099
return
2100
if mesg['event'] != 'start_session':
2101
log("Received an unknown message event = %s; terminating session." %
2102
mesg['event'])
2103
return
2104
2105
log("Starting a session")
2106
desc = message.session_description(os.getpid())
2107
log("child sending session description back: %s" % desc)
2108
conn.send_json(desc)
2109
session(conn=conn)
2110
2111
2112
def serve(port, host, extra_imports=False):
2113
#log.info('opening connection on port %s', port)
2114
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
2115
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
2116
2117
# check for children that have finished every few seconds, so
2118
# we don't end up with zombies.
2119
s.settimeout(5)
2120
2121
s.bind((host, port))
2122
log('Sage server %s:%s' % (host, port))
2123
2124
# Enabling the following signal completely breaks subprocess pexpect in many cases, which is
2125
# obviously totally unacceptable.
2126
#signal.signal(signal.SIGCHLD, handle_session_term)
2127
2128
def init_library():
2129
tm = time.time()
2130
log("pre-importing the sage library...")
2131
2132
# FOR testing purposes.
2133
##log("fake 40 second pause to slow things down for testing....")
2134
##time.sleep(40)
2135
##log("done with pause")
2136
2137
# Actually import sage now. This must happen after the interact
2138
# import because of library interacts.
2139
log("import sage...")
2140
import sage.all
2141
log("imported sage.")
2142
2143
# Monkey patching interact using the new and improved Salvus
2144
# implementation of interact.
2145
sage.all.interact = sage_salvus.interact
2146
2147
# Monkey patch the html command.
2148
try:
2149
# need the following for sage_server to start with sage-8.0
2150
# or `import sage.interacts.library` will fail (not really important below, as we don't do that).
2151
import sage.repl.user_globals
2152
sage.repl.user_globals.set_globals(globals())
2153
log("initialized user_globals")
2154
except RuntimeError:
2155
# may happen with sage version < 8.0
2156
log("user_globals.set_globals failed, continuing", sys.exc_info())
2157
2158
sage.all.html = sage.misc.html.html = sage_salvus.html
2159
2160
# CRITICAL: look, we are just going to not do this, and have sage.interacts.library
2161
# be broken. It's **really slow** to do this, and I don't think sage.interacts.library
2162
# ever ended up going anywhere! People use wiki.sagemath.org/interact instead...
2163
#import sage.interacts.library
2164
#sage.interacts.library.html = sage_salvus.html
2165
2166
# Set a useful figsize default; the matplotlib one is not notebook friendly.
2167
import sage.plot.graphics
2168
sage.plot.graphics.Graphics.SHOW_OPTIONS['figsize'] = [8, 4]
2169
2170
# Monkey patch latex.eval, so that %latex works in worksheets
2171
sage.misc.latex.latex.eval = sage_salvus.latex0
2172
2173
# Plot, integrate, etc., -- so startup time of worksheets is minimal.
2174
cmds = [
2175
'from sage.all import *', 'from sage.calculus.predefined import x',
2176
'import pylab'
2177
]
2178
if extra_imports:
2179
cmds.extend([
2180
'import scipy', 'import sympy',
2181
"plot(sin).save('%s/a.png'%os.environ['SMC'], figsize=2)",
2182
'integrate(sin(x**2),x)'
2183
])
2184
tm0 = time.time()
2185
for cmd in cmds:
2186
log(cmd)
2187
exec(cmd, namespace)
2188
2189
global pylab
2190
pylab = namespace['pylab'] # used for clearing
2191
2192
log('imported sage library and other components in %s seconds' %
2193
(time.time() - tm))
2194
2195
for k, v in sage_salvus.interact_functions.items():
2196
namespace[k] = v
2197
# See above -- not doing this, since it is REALLY SLOW to import.
2198
# This does mean that some old code that tries to use interact might break (?).
2199
#namespace[k] = sagenb.notebook.interact.__dict__[k] = v
2200
2201
namespace['_salvus_parsing'] = sage_parsing
2202
2203
for name in [
2204
'anaconda', 'asy', 'attach', 'auto', 'capture', 'cell',
2205
'clear', 'coffeescript', 'cython', 'default_mode',
2206
'delete_last_output', 'dynamic', 'exercise', 'fork', 'fortran',
2207
'go', 'help', 'hide', 'hideall', 'input', 'java', 'javascript',
2208
'julia', 'jupyter', 'license', 'load', 'md', 'mediawiki',
2209
'modes', 'octave', 'pandoc', 'perl', 'plot3d_using_matplotlib',
2210
'prun', 'python_future_feature', 'py3print_mode', 'python',
2211
'python3', 'r', 'raw_input', 'reset', 'restore', 'ruby',
2212
'runfile', 'sage_eval', 'scala', 'scala211', 'script',
2213
'search_doc', 'search_src', 'sh', 'show', 'show_identifiers',
2214
'singular_kernel', 'time', 'timeit', 'typeset_mode', 'var',
2215
'wiki'
2216
]:
2217
namespace[name] = getattr(sage_salvus, name)
2218
2219
namespace['sage_server'] = sys.modules[
2220
__name__] # http://stackoverflow.com/questions/1676835/python-how-do-i-get-a-reference-to-a-module-inside-the-module-itself
2221
2222
# alias pretty_print_default to typeset_mode, since sagenb has/uses that.
2223
namespace['pretty_print_default'] = namespace['typeset_mode']
2224
# and monkey patch it
2225
sage.misc.latex.pretty_print_default = namespace[
2226
'pretty_print_default']
2227
2228
sage_salvus.default_namespace = dict(namespace)
2229
log("setup namespace with extra functions")
2230
2231
# Sage's pretty_print and view are both ancient and a mess
2232
sage.all.pretty_print = sage.misc.latex.pretty_print = namespace[
2233
'pretty_print'] = namespace['view'] = namespace['show']
2234
2235
# this way client code can tell it is running as a Sage Worksheet.
2236
namespace['__SAGEWS__'] = True
2237
2238
log("Initialize sage library.")
2239
init_library()
2240
2241
t = time.time()
2242
s.listen(128)
2243
i = 0
2244
2245
children = {}
2246
log("Starting server listening for connections")
2247
try:
2248
while True:
2249
i += 1
2250
#print i, time.time()-t, 'cps: ', int(i/(time.time()-t))
2251
# do not use log.info(...) in the server loop; threads = race conditions that hang server every so often!!
2252
try:
2253
if children:
2254
for pid in list(children.keys()):
2255
if os.waitpid(pid, os.WNOHANG) != (0, 0):
2256
log("subprocess %s terminated, closing connection"
2257
% pid)
2258
conn.close()
2259
del children[pid]
2260
2261
try:
2262
conn, addr = s.accept()
2263
log("Accepted a connection from", addr)
2264
except:
2265
# this will happen periodically since we did s.settimeout above, so
2266
# that we wait for children above periodically.
2267
continue
2268
except socket.error:
2269
continue
2270
child_pid = os.fork()
2271
if child_pid: # parent
2272
log("forked off child with pid %s to handle this connection" %
2273
child_pid)
2274
children[child_pid] = conn
2275
else:
2276
# child
2277
global PID
2278
PID = os.getpid()
2279
log("child process, will now serve this new connection")
2280
serve_connection(conn)
2281
2282
# end while
2283
except Exception as err:
2284
log("Error taking connection: ", err)
2285
traceback.print_exc(file=open(LOGFILE, 'a'))
2286
#log.error("error: %s %s", type(err), str(err))
2287
2288
finally:
2289
log("closing socket")
2290
#s.shutdown(0)
2291
s.close()
2292
2293
2294
def run_server(port, host, pidfile, logfile=None):
2295
global LOGFILE
2296
if logfile:
2297
LOGFILE = logfile
2298
if pidfile:
2299
pid = str(os.getpid())
2300
print("os.getpid() = %s" % pid)
2301
open(pidfile, 'w').write(pid)
2302
log("run_server: port=%s, host=%s, pidfile='%s', logfile='%s'" %
2303
(port, host, pidfile, LOGFILE))
2304
try:
2305
serve(port, host)
2306
finally:
2307
if pidfile:
2308
os.unlink(pidfile)
2309
2310
2311
if __name__ == "__main__":
2312
import argparse
2313
parser = argparse.ArgumentParser(description="Run Sage server")
2314
parser.add_argument(
2315
"-p",
2316
dest="port",
2317
type=int,
2318
default=0,
2319
help=
2320
"port to listen on (default: 0); 0 = automatically allocated; saved to $SMC/data/sage_server.port"
2321
)
2322
parser.add_argument(
2323
"-l",
2324
dest='log_level',
2325
type=str,
2326
default='INFO',
2327
help=
2328
"log level (default: INFO) useful options include WARNING and DEBUG")
2329
parser.add_argument("-d",
2330
dest="daemon",
2331
default=False,
2332
action="store_const",
2333
const=True,
2334
help="daemon mode (default: False)")
2335
parser.add_argument(
2336
"--host",
2337
dest="host",
2338
type=str,
2339
default='127.0.0.1',
2340
help="host interface to bind to -- default is 127.0.0.1")
2341
parser.add_argument("--pidfile",
2342
dest="pidfile",
2343
type=str,
2344
default='',
2345
help="store pid in this file")
2346
parser.add_argument(
2347
"--logfile",
2348
dest="logfile",
2349
type=str,
2350
default='',
2351
help="store log in this file (default: '' = don't log to a file)")
2352
parser.add_argument("-c",
2353
dest="client",
2354
default=False,
2355
action="store_const",
2356
const=True,
2357
help="run in test client mode number 1 (command line)")
2358
parser.add_argument("--hostname",
2359
dest="hostname",
2360
type=str,
2361
default='',
2362
help="hostname to connect to in client mode")
2363
parser.add_argument("--portfile",
2364
dest="portfile",
2365
type=str,
2366
default='',
2367
help="write port to this file")
2368
2369
args = parser.parse_args()
2370
2371
if args.daemon and not args.pidfile:
2372
print(("%s: must specify pidfile in daemon mode" % sys.argv[0]))
2373
sys.exit(1)
2374
2375
if args.log_level:
2376
pass
2377
#level = getattr(logging, args.log_level.upper())
2378
#log.setLevel(level)
2379
2380
if args.client:
2381
client1(
2382
port=args.port if args.port else int(open(args.portfile).read()),
2383
hostname=args.hostname)
2384
sys.exit(0)
2385
2386
if not args.port:
2387
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
2388
s.bind(('', 0)) # pick a free port
2389
args.port = s.getsockname()[1]
2390
del s
2391
2392
if args.portfile:
2393
open(args.portfile, 'w').write(str(args.port))
2394
2395
pidfile = os.path.abspath(args.pidfile) if args.pidfile else ''
2396
logfile = os.path.abspath(args.logfile) if args.logfile else ''
2397
if logfile:
2398
LOGFILE = logfile
2399
open(LOGFILE, 'w') # for now we clear it on restart...
2400
log("setting logfile to %s" % LOGFILE)
2401
2402
main = lambda: run_server(port=args.port, host=args.host, pidfile=pidfile)
2403
if args.daemon and args.pidfile:
2404
from . import daemon
2405
daemon.daemonize(args.pidfile)
2406
main()
2407
else:
2408
main()
2409
2410