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