Path: blob/master/src/smc_sagews/smc_sagews/sage_server.py
Views: 286
#!/usr/bin/env python1"""2sage_server.py -- unencrypted forking TCP server.34Note: I wrote functionality so this can run as root, create accounts on the fly,5and serve sage as those accounts. Doing this is horrendous from a security point of6view, and I'm definitely not doing this.78None of that functionality is actually used in https://cocalc.com!910For debugging, this may help:1112killemall sage_server.py && sage --python sage_server.py -p 60001314"""1516# NOTE: This file is GPL'd17# because it imports the Sage library. This file is not directly18# imported by anything else in CoCalc; the Python process it runs is19# used over a TCP connection.2021#########################################################################################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#########################################################################################2829# Add the path that contains this file to the Python load path, so we30# can import other files from there.31from __future__ import absolute_import32import six33import os, sys, time, operator34import __future__ as future35from functools import reduce363738def is_string(s):39return isinstance(s, six.string_types)404142def unicode8(s):43# I evidently don't understand Python unicode... Do the following for now:44# TODO: see http://stackoverflow.com/questions/21897664/why-does-unicodeu-passed-an-errors-parameter-raise-typeerror for how to fix.45try:46if six.PY2:47return str(s).encode('utf-8')48else:49return str(s, 'utf-8')50except:51try:52return str(s)53except:54return s555657LOGFILE = os.path.realpath(__file__)[:-3] + ".log"58PID = os.getpid()59from datetime import datetime606162def log(*args):63try:64debug_log = open(LOGFILE, 'a')65mesg = "%s (%s): %s\n" % (PID, datetime.utcnow().strftime(66'%Y-%m-%d %H:%M:%S.%f')[:-3], ' '.join([unicode8(x)67for x in args]))68debug_log.write(mesg)69debug_log.flush()70except Exception as err:71print(("an error writing a log message (ignoring) -- %s" % err, args))727374# used for clearing pylab figure75pylab = None7677# Maximum number of distinct (non-once) output messages per cell; when this number is78# exceeded, an exception is raised; this reduces the chances of the user creating79# a huge unusable worksheet.80MAX_OUTPUT_MESSAGES = 25681# stdout, stderr, html, etc. that exceeds this many characters will be truncated to avoid82# killing the client.83MAX_STDOUT_SIZE = MAX_STDERR_SIZE = MAX_CODE_SIZE = MAX_HTML_SIZE = MAX_MD_SIZE = MAX_TEX_SIZE = 400008485MAX_OUTPUT = 1500008687# Standard imports.88import json, resource, shutil, signal, socket, struct, \89tempfile, time, traceback, pwd, re9091# for "3x^2 + 4xy - 5(1+x) - 3 abc4ok", this pattern matches "3x", "5(" and "4xy" but not "abc4ok"92# to understand it, see https://regex101.com/ or https://www.debuggex.com/93RE_POSSIBLE_IMPLICIT_MUL = re.compile(r'(?:(?<=[^a-zA-Z])|^)(\d+[a-zA-Z\(]+)')9495try:96from . import sage_parsing, sage_salvus97except:98import sage_parsing, sage_salvus99100uuid = sage_salvus.uuid101102reload_attached_files_if_mod_smc_available = True103104105def reload_attached_files_if_mod_smc():106# CRITICAL: do NOT impor sage.repl.attach!! That will import IPython, wasting several seconds and107# killing the user experience for no reason.108try:109import sage.repl110sage.repl.attach111except:112# nothing to do -- attach has not been used and is not yet available.113return114global reload_attached_files_if_mod_smc_available115if not reload_attached_files_if_mod_smc_available:116return117try:118from sage.repl.attach import load_attach_path, modified_file_iterator119except:120print("sage_server: attach not available")121reload_attached_files_if_mod_smc_available = False122return123# see sage/src/sage/repl/attach.py reload_attached_files_if_modified()124for filename, mtime in modified_file_iterator():125basename = os.path.basename(filename)126timestr = time.strftime('%T', mtime)127log('reloading attached file {0} modified at {1}'.format(128basename, timestr))129from .sage_salvus import load130load(filename)131132133# Determine the info object, if available. There's no good reason134# it wouldn't be available, unless a user explicitly deleted it, but135# we may as well try to be robust to this, especially if somebody136# were to try to use this server outside of cloud.sagemath.com.137_info_path = os.path.join(os.environ['SMC'], 'info.json')138if os.path.exists(_info_path):139try:140INFO = json.loads(open(_info_path).read())141except:142# This will fail, e.g., if info.json is invalid (maybe a blank file).143# We definitely don't want sage server startup to be completely broken144# in this case, so we fall back to "no info".145INFO = {}146else:147INFO = {}148if 'base_url' not in INFO:149INFO['base_url'] = ''150151# Configure logging152#logging.basicConfig()153#log = logging.getLogger('sage_server')154#log.setLevel(logging.INFO)155156# A CoffeeScript version of this function is in misc_node.coffee.157import hashlib158159160def uuidsha1(data):161sha1sum = hashlib.sha1()162sha1sum.update(data)163s = sha1sum.hexdigest()164t = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'165r = list(t)166j = 0167for i in range(len(t)):168if t[i] == 'x':169r[i] = s[j]170j += 1171elif t[i] == 'y':172# take 8 + low order 3 bits of hex number.173r[i] = hex((int(s[j], 16) & 0x3) | 0x8)[-1]174j += 1175return ''.join(r)176177178# A tcp connection with support for sending various types of messages, especially JSON.179class ConnectionJSON(object):180181def __init__(self, conn):182# avoid common mistake -- conn is supposed to be from socket.socket...183assert not isinstance(conn, ConnectionJSON)184self._conn = conn185186def close(self):187self._conn.close()188189def _send(self, s):190if six.PY3 and type(s) == str:191s = s.encode('utf8')192length_header = struct.pack(">L", len(s))193# py3: TypeError: can't concat str to bytes194self._conn.send(length_header + s)195196def send_json(self, m):197m = json.dumps(m)198if '\\u0000' in m:199raise RuntimeError("NULL bytes not allowed")200log("sending message '", truncate_text(m, 256), "'")201self._send('j' + m)202return len(m)203204def send_blob(self, blob):205if six.PY3 and type(blob) == str:206# unicode objects must be encoded before hashing207blob = blob.encode('utf8')208209s = uuidsha1(blob)210if six.PY3 and type(blob) == bytes:211# we convert all to bytes first, to avoid unnecessary conversions212self._send(('b' + s).encode('utf8') + blob)213else:214# old sage py2 code215self._send('b' + s + blob)216return s217218def send_file(self, filename):219log("sending file '%s'" % filename)220f = open(filename, 'rb')221data = f.read()222f.close()223return self.send_blob(data)224225def _recv(self, n):226#print("_recv(%s)"%n)227# see http://stackoverflow.com/questions/3016369/catching-blocking-sigint-during-system-call228for i in range(20):229try:230#print "blocking recv (i = %s), pid=%s"%(i, os.getpid())231r = self._conn.recv(n)232#log("n=%s; received: '%s' of len %s"%(n,r, len(r)))233return r234except OSError as e:235#print("socket.error, msg=%s"%msg)236if e.errno != 4:237raise238raise EOFError239240def recv(self):241n = self._recv(4)242if len(n) < 4:243raise EOFError244n = struct.unpack('>L', n)[0] # big endian 32 bits245s = self._recv(n)246while len(s) < n:247t = self._recv(n - len(s))248if len(t) == 0:249raise EOFError250s += t251252if six.PY3:253# bystream to string, in particular s[0] will be e.g. 'j' and not 106254#log("ConnectionJSON::recv s=%s... (type %s)" % (s[:5], type(s)))255# is s always of type bytes?256if type(s) == bytes:257s = s.decode('utf8')258259if s[0] == 'j':260try:261return 'json', json.loads(s[1:])262except Exception as msg:263log("Unable to parse JSON '%s'" % s[1:])264raise265266elif s[0] == 'b':267return 'blob', s[1:]268raise ValueError("unknown message type '%s'" % s[0])269270271def truncate_text(s, max_size):272if len(s) > max_size:273return s[:max_size] + "[...]", True274else:275return s, False276277278def truncate_text_warn(s, max_size, name):279r"""280Truncate text if too long and format a warning message.281282INPUT:283284- ``s`` -- string to be truncated285- ``max-size`` - integer truncation limit286- ``name`` - string, name of limiting parameter287288OUTPUT:289290a triple:291292- string -- possibly truncated input string293- boolean -- true if input string was truncated294- string -- warning message if input string was truncated295"""296tmsg = "WARNING: Output: %s truncated by %s to %s. Type 'smc?' to learn how to raise the output limit."297lns = len(s)298if lns > max_size:299tmsg = tmsg % (lns, name, max_size)300return s[:max_size] + "[...]", True, tmsg301else:302return s, False, ''303304305class Message(object):306307def _new(self, event, props={}):308m = {'event': event}309for key, val in props.items():310if key != 'self':311m[key] = val312return m313314def start_session(self):315return self._new('start_session')316317def session_description(self, pid):318return self._new('session_description', {'pid': pid})319320def send_signal(self, pid, signal=signal.SIGINT):321return self._new('send_signal', locals())322323def terminate_session(self, done=True):324return self._new('terminate_session', locals())325326def execute_code(self, id, code, preparse=True):327return self._new('execute_code', locals())328329def execute_javascript(self, code, obj=None, coffeescript=False):330return self._new('execute_javascript', locals())331332def output(333self,334id,335stdout=None,336stderr=None,337code=None,338html=None,339javascript=None,340coffeescript=None,341interact=None,342md=None,343tex=None,344d3=None,345file=None,346raw_input=None,347obj=None,348once=None,349hide=None,350show=None,351events=None,352clear=None,353delete_last=None,354done=False # CRITICAL: done must be specified for multi-response; this is assumed by sage_session.coffee; otherwise response assumed single.355):356m = self._new('output')357m['id'] = id358t = truncate_text_warn359did_truncate = False360from . import sage_server # we do this so that the user can customize the MAX's below.361if code is not None:362code['source'], did_truncate, tmsg = t(code['source'],363sage_server.MAX_CODE_SIZE,364'MAX_CODE_SIZE')365m['code'] = code366if stderr is not None and len(stderr) > 0:367m['stderr'], did_truncate, tmsg = t(stderr,368sage_server.MAX_STDERR_SIZE,369'MAX_STDERR_SIZE')370if stdout is not None and len(stdout) > 0:371m['stdout'], did_truncate, tmsg = t(stdout,372sage_server.MAX_STDOUT_SIZE,373'MAX_STDOUT_SIZE')374if html is not None and len(html) > 0:375m['html'], did_truncate, tmsg = t(html, sage_server.MAX_HTML_SIZE,376'MAX_HTML_SIZE')377if md is not None and len(md) > 0:378m['md'], did_truncate, tmsg = t(md, sage_server.MAX_MD_SIZE,379'MAX_MD_SIZE')380if tex is not None and len(tex) > 0:381tex['tex'], did_truncate, tmsg = t(tex['tex'],382sage_server.MAX_TEX_SIZE,383'MAX_TEX_SIZE')384m['tex'] = tex385if javascript is not None: m['javascript'] = javascript386if coffeescript is not None: m['coffeescript'] = coffeescript387if interact is not None: m['interact'] = interact388if d3 is not None: m['d3'] = d3389if obj is not None: m['obj'] = json.dumps(obj)390if file is not None: m['file'] = file # = {'filename':..., 'uuid':...}391if raw_input is not None: m['raw_input'] = raw_input392if done is not None: m['done'] = done393if once is not None: m['once'] = once394if hide is not None: m['hide'] = hide395if show is not None: m['show'] = show396if events is not None: m['events'] = events397if clear is not None: m['clear'] = clear398if delete_last is not None: m['delete_last'] = delete_last399if did_truncate:400if 'stderr' in m:401m['stderr'] += '\n' + tmsg402else:403m['stderr'] = '\n' + tmsg404return m405406def introspect_completions(self, id, completions, target):407m = self._new('introspect_completions', locals())408m['id'] = id409return m410411def introspect_docstring(self, id, docstring, target):412m = self._new('introspect_docstring', locals())413m['id'] = id414return m415416def introspect_source_code(self, id, source_code, target):417m = self._new('introspect_source_code', locals())418m['id'] = id419return m420421422message = Message()423424whoami = os.environ['USER']425426427def client1(port, hostname):428conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)429conn.connect((hostname, int(port)))430conn = ConnectionJSON(conn)431432conn.send_json(message.start_session())433typ, mesg = conn.recv()434pid = mesg['pid']435print(("PID = %s" % pid))436437id = 0438while True:439try:440code = sage_parsing.get_input('sage [%s]: ' % id)441if code is None: # EOF442break443conn.send_json(message.execute_code(code=code, id=id))444while True:445typ, mesg = conn.recv()446if mesg['event'] == 'terminate_session':447return448elif mesg['event'] == 'output':449if 'stdout' in mesg:450sys.stdout.write(mesg['stdout'])451sys.stdout.flush()452if 'stderr' in mesg:453print(('! ' +454'\n! '.join(mesg['stderr'].splitlines())))455if 'done' in mesg and mesg['id'] >= id:456break457id += 1458459except KeyboardInterrupt:460print("Sending interrupt signal")461conn2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)462conn2.connect((hostname, int(port)))463conn2 = ConnectionJSON(conn2)464conn2.send_json(message.send_signal(pid))465del conn2466id += 1467468conn.send_json(message.terminate_session())469print("\nExiting Sage client.")470471472class BufferedOutputStream(object):473474def __init__(self, f, flush_size=4096, flush_interval=.1):475self._f = f476self._buf = ''477self._flush_size = flush_size478self._flush_interval = flush_interval479self.reset()480481def reset(self):482self._last_flush_time = time.time()483484def fileno(self):485return 0486487def write(self, output):488# CRITICAL: we need output to valid PostgreSQL TEXT, so no null bytes489# This is not going to silently corrupt anything -- it's just output that490# is destined to be *rendered* in the browser. This is only a partial491# solution to a more general problem, but it is safe.492try:493self._buf += output.replace('\x00', '')494except UnicodeDecodeError:495self._buf += output.decode('utf-8').replace('\x00', '')496#self.flush()497t = time.time()498if ((len(self._buf) >= self._flush_size)499or (t - self._last_flush_time >= self._flush_interval)):500self.flush()501self._last_flush_time = t502503def flush(self, done=False):504if not self._buf and not done:505# no point in sending an empty message506return507try:508self._f(self._buf, done=done)509except UnicodeDecodeError:510if six.PY2: # str doesn't have errors option in python2!511self._f(unicode(self._buf, errors='replace'), done=done)512else:513self._f(str(self._buf, errors='replace'), done=done)514self._buf = ''515516def isatty(self):517return False518519520# This will *have* to be re-done using Cython for speed.521class Namespace(dict):522523def __init__(self, x):524self._on_change = {}525self._on_del = {}526dict.__init__(self, x)527528def on(self, event, x, f):529if event == 'change':530if x not in self._on_change:531self._on_change[x] = []532self._on_change[x].append(f)533elif event == 'del':534if x not in self._on_del:535self._on_del[x] = []536self._on_del[x].append(f)537538def remove(self, event, x, f):539if event == 'change' and x in self._on_change:540v = self._on_change[x]541i = v.find(f)542if i != -1:543del v[i]544if len(v) == 0:545del self._on_change[x]546elif event == 'del' and x in self._on_del:547v = self._on_del[x]548i = v.find(f)549if i != -1:550del v[i]551if len(v) == 0:552del self._on_del[x]553554def __setitem__(self, x, y):555dict.__setitem__(self, x, y)556try:557if x in self._on_change:558for f in self._on_change[x]:559f(y)560if None in self._on_change:561for f in self._on_change[None]:562f(x, y)563except Exception as mesg:564print(mesg)565566def __delitem__(self, x):567try:568if x in self._on_del:569for f in self._on_del[x]:570f()571if None in self._on_del:572for f in self._on_del[None]:573f(x)574except Exception as mesg:575print(mesg)576dict.__delitem__(self, x)577578def set(self, x, y, do_not_trigger=None):579dict.__setitem__(self, x, y)580if x in self._on_change:581if do_not_trigger is None:582do_not_trigger = []583for f in self._on_change[x]:584if f not in do_not_trigger:585f(y)586if None in self._on_change:587for f in self._on_change[None]:588f(x, y)589590591class TemporaryURL:592593def __init__(self, url, ttl):594self.url = url595self.ttl = ttl596597def __repr__(self):598return repr(self.url)599600def __str__(self):601return self.url602603604namespace = Namespace({})605606607class Salvus(object):608"""609Cell execution state object and wrapper for access to special CoCalc Server functionality.610611An instance of this object is created each time you execute a cell. It has various methods612for sending different types of output messages, links to files, etc. Type 'help(smc)' for613more details.614615OUTPUT LIMITATIONS -- There is an absolute limit on the number of messages output for a given616cell, and also the size of the output message for each cell. You can access or change617those limits dynamically in a worksheet as follows by viewing or changing any of the618following variables::619620sage_server.MAX_STDOUT_SIZE # max length of each stdout output message621sage_server.MAX_STDERR_SIZE # max length of each stderr output message622sage_server.MAX_MD_SIZE # max length of each md (markdown) output message623sage_server.MAX_HTML_SIZE # max length of each html output message624sage_server.MAX_TEX_SIZE # max length of tex output message625sage_server.MAX_OUTPUT_MESSAGES # max number of messages output for a cell.626627And::628629sage_server.MAX_OUTPUT # max total character output for a single cell; computation630# terminated/truncated if sum of above exceeds this.631"""632Namespace = Namespace633_prefix = ''634_postfix = ''635_default_mode = 'sage'636_py_features = {}637638def _flush_stdio(self):639"""640Flush the standard output streams. This should be called before sending any message641that produces output.642"""643sys.stdout.flush()644sys.stderr.flush()645646def __repr__(self):647return ''648649def __init__(self, conn, id, data=None, cell_id=None, message_queue=None):650self._conn = conn651self._num_output_messages = 0652self._total_output_length = 0653self._output_warning_sent = False654self._id = id655self._done = True # done=self._done when last execute message is sent; e.g., set self._done = False to not close cell on code term.656self.data = data657self.cell_id = cell_id658self.namespace = namespace659self.message_queue = message_queue660self.code_decorators = [] # gets reset if there are code decorators661# Alias: someday remove all references to "salvus" and instead use smc.662# For now this alias is easier to think of and use.663namespace['smc'] = namespace[664'salvus'] = self # beware of circular ref?665# Monkey patch in our "require" command.666namespace['require'] = self.require667# Make the salvus object itself available when doing "from sage.all import *".668import sage.all669sage.all.salvus = self670671def _send_output(self, *args, **kwds):672if self._output_warning_sent:673raise KeyboardInterrupt674mesg = message.output(*args, **kwds)675if not mesg.get('once', False):676self._num_output_messages += 1677from . import sage_server678679if self._num_output_messages > sage_server.MAX_OUTPUT_MESSAGES:680self._output_warning_sent = True681err = "\nToo many output messages: %s (at most %s per cell -- type 'smc?' to learn how to raise this limit): attempting to terminate..." % (682self._num_output_messages, sage_server.MAX_OUTPUT_MESSAGES)683self._conn.send_json(684message.output(stderr=err, id=self._id, once=False, done=True))685raise KeyboardInterrupt686687n = self._conn.send_json(mesg)688self._total_output_length += n689690if self._total_output_length > sage_server.MAX_OUTPUT:691self._output_warning_sent = True692err = "\nOutput too long: %s -- MAX_OUTPUT (=%s) exceeded (type 'smc?' to learn how to raise this limit): attempting to terminate..." % (693self._total_output_length, sage_server.MAX_OUTPUT)694self._conn.send_json(695message.output(stderr=err, id=self._id, once=False, done=True))696raise KeyboardInterrupt697698def obj(self, obj, done=False):699self._send_output(obj=obj, id=self._id, done=done)700return self701702def link(self, filename, label=None, foreground=True, cls=''):703"""704Output a clickable link to a file somewhere in this project. The filename705path must be relative to the current working directory of the Python process.706707The simplest way to use this is708709salvus.link("../name/of/file") # any relative path to any file710711This creates a link, which when clicked on, opens that file in the foreground.712713If the filename is the name of a directory, clicking will instead714open the file browser on that directory:715716salvus.link("../name/of/directory") # clicking on the resulting link opens a directory717718If you would like a button instead of a link, pass cls='btn'. You can use any of719the standard Bootstrap button classes, e.g., btn-small, btn-large, btn-success, etc.720721If you would like to change the text in the link (or button) to something722besides the default (filename), just pass arbitrary HTML to the label= option.723724INPUT:725726- filename -- a relative path to a file or directory727- label -- (default: the filename) html label for the link728- foreground -- (default: True); if True, opens link in the foreground729- cls -- (default: '') optional CSS classes, such as 'btn'.730731EXAMPLES:732733Use as a line decorator::734735%salvus.link name/of/file.foo736737Make a button::738739salvus.link("foo/bar/", label="The Bar Directory", cls='btn')740741Make two big blue buttons with plots in them::742743plot(sin, 0, 20).save('sin.png')744plot(cos, 0, 20).save('cos.png')745for img in ['sin.png', 'cos.png']:746salvus.link(img, label="<img width='150px' src='%s'>"%salvus.file(img, show=False), cls='btn btn-large btn-primary')747748749750"""751path = os.path.abspath(filename)[len(os.environ['HOME']) + 1:]752if label is None:753label = filename754id = uuid()755self.html("<a class='%s' style='cursor:pointer'; id='%s'></a>" %756(cls, id))757758s = "$('#%s').html(obj.label).click(function() {%s; return false;});" % (759id, self._action(path, foreground))760self.javascript(s,761obj={762'label': label,763'path': path,764'foreground': foreground765},766once=False)767768def _action(self, path, foreground):769if os.path.isdir(path):770if foreground:771action = "worksheet.project_page.open_directory(obj.path);"772else:773action = "worksheet.project_page.set_current_path(obj.path);"774else:775action = "worksheet.project_page.open_file({'path':obj.path, 'foreground': obj.foreground});"776return action777778def open_tab(self, filename, foreground=True):779"""780Open a new file (or directory) document in another tab.781See the documentation for salvus.link.782"""783path = os.path.abspath(filename)[len(os.environ['HOME']) + 1:]784self.javascript(self._action(path, foreground),785obj={786'path': path,787'foreground': foreground788},789once=True)790791def close_tab(self, filename):792"""793Close an open file tab. The filename is relative to the current working directory.794"""795self.javascript("worksheet.project_page.close_file(obj)",796obj=filename,797once=True)798799def threed(800self,801g, # sage Graphic3d object.802width=None,803height=None,804frame=True, # True/False or {'color':'black', 'thickness':.4, 'labels':True, 'fontsize':14, 'draw':True,805# 'xmin':?, 'xmax':?, 'ymin':?, 'ymax':?, 'zmin':?, 'zmax':?}806background=None,807foreground=None,808spin=False,809aspect_ratio=None,810frame_aspect_ratio=None, # synonym for aspect_ratio811done=False,812renderer=None, # None, 'webgl', or 'canvas'813):814815from .graphics import graphics3d_to_jsonable, json_float as f816817# process options, combining ones set explicitly above with ones inherited from 3d scene818opts = {819'width': width,820'height': height,821'background': background,822'foreground': foreground,823'spin': spin,824'aspect_ratio': aspect_ratio,825'renderer': renderer826}827828extra_kwds = {} if g._extra_kwds is None else g._extra_kwds829830# clean up and normalize aspect_ratio option831if aspect_ratio is None:832if frame_aspect_ratio is not None:833aspect_ratio = frame_aspect_ratio834elif 'frame_aspect_ratio' in extra_kwds:835aspect_ratio = extra_kwds['frame_aspect_ratio']836elif 'aspect_ratio' in extra_kwds:837aspect_ratio = extra_kwds['aspect_ratio']838if aspect_ratio is not None:839if aspect_ratio == 1 or aspect_ratio == "automatic":840aspect_ratio = None841elif not (isinstance(aspect_ratio,842(list, tuple)) and len(aspect_ratio) == 3):843raise TypeError(844"aspect_ratio must be None, 1 or a 3-tuple, but it is '%s'"845% (aspect_ratio, ))846else:847aspect_ratio = [f(x) for x in aspect_ratio]848849opts['aspect_ratio'] = aspect_ratio850851for k in [852'spin',853'height',854'width',855'background',856'foreground',857'renderer',858]:859if k in extra_kwds and not opts.get(k, None):860opts[k] = extra_kwds[k]861862if not isinstance(opts['spin'], bool):863opts['spin'] = f(opts['spin'])864opts['width'] = f(opts['width'])865opts['height'] = f(opts['height'])866867# determine the frame868b = g.bounding_box()869xmin, xmax, ymin, ymax, zmin, zmax = b[0][0], b[1][0], b[0][1], b[1][8701], b[0][2], b[1][2]871fr = opts['frame'] = {872'xmin': f(xmin),873'xmax': f(xmax),874'ymin': f(ymin),875'ymax': f(ymax),876'zmin': f(zmin),877'zmax': f(zmax)878}879880if isinstance(frame, dict):881for k in list(fr.keys()):882if k in frame:883fr[k] = f(frame[k])884fr['draw'] = frame.get('draw', True)885fr['color'] = frame.get('color', None)886fr['thickness'] = f(frame.get('thickness', None))887fr['labels'] = frame.get('labels', None)888if 'fontsize' in frame:889fr['fontsize'] = int(frame['fontsize'])890elif isinstance(frame, bool):891fr['draw'] = frame892893# convert the Sage graphics object to a JSON object that can be rendered894scene = {'opts': opts, 'obj': graphics3d_to_jsonable(g)}895896# Store that object in the database, rather than sending it directly as an output message.897# We do this since obj can easily be quite large/complicated, and managing it as part of the898# document is too slow and doesn't scale.899blob = json.dumps(scene, separators=(',', ':'))900uuid = self._conn.send_blob(blob)901902# flush output (so any text appears before 3d graphics, in case they are interleaved)903self._flush_stdio()904905# send message pointing to the 3d 'file', which will get downloaded from database906self._send_output(id=self._id,907file={908'filename': unicode8("%s.sage3d" % uuid),909'uuid': uuid910},911done=done)912913def d3_graph(self, g, **kwds):914from .graphics import graph_to_d3_jsonable915self._send_output(id=self._id,916d3={917"viewer": "graph",918"data": graph_to_d3_jsonable(g, **kwds)919})920921def file(self,922filename,923show=True,924done=False,925download=False,926once=False,927events=None,928raw=False,929text=None):930"""931Display or provide a link to the given file. Raises a RuntimeError if this932is not possible, e.g, if the file is too large.933934If show=True (the default), the browser will show the file,935or provide a clickable link to it if there is no way to show it.936If text is also given that will be used instead of the path to the file.937938If show=False, this function returns an object T such that939T.url (or str(t)) is a string of the form "/blobs/filename?uuid=the_uuid"940that can be used to access the file even if the file is immediately941deleted after calling this function (the file is stored in a database).942Also, T.ttl is the time to live (in seconds) of the object. A ttl of9430 means the object is permanently available.944945raw=False (the default):946If you use the URL947/blobs/filename?uuid=the_uuid&download948then the server will include a header that tells the browser to949download the file to disk instead of displaying it. Only relatively950small files can be made available this way. However, they remain951available (for a day) even *after* the file is deleted.952NOTE: It is safe to delete the file immediately after this953function (salvus.file) returns.954955raw=True:956Instead, the URL is to the raw file, which is served directly957from the project:958/project-id/raw/path/to/filename959This will only work if the file is not deleted; however, arbitrarily960large files can be streamed this way. This is useful for animations.961962This function creates an output message {file:...}; if the user saves963a worksheet containing this message, then any referenced blobs are made964permanent in the database.965966The uuid is based on the Sha-1 hash of the file content (it is computed using the967function sage_server.uuidsha1). Any two files with the same content have the968same Sha1 hash.969970The file does NOT have to be in the HOME directory.971"""972filename = unicode8(filename)973if raw:974info = self.project_info()975path = os.path.abspath(filename)976home = os.environ['HOME'] + '/'977978if not path.startswith(home):979# Attempt to use the $HOME/.smc/root symlink instead:980path = os.path.join(os.environ['HOME'], '.smc', 'root',981path.lstrip('/'))982983if path.startswith(home):984path = path[len(home):]985else:986raise ValueError(987"can only send raw files in your home directory -- path='%s'"988% path)989url = os.path.join('/', info['base_url'].strip('/'),990info['project_id'], 'raw', path.lstrip('/'))991if show:992self._flush_stdio()993self._send_output(id=self._id,994once=once,995file={996'filename': filename,997'url': url,998'show': show,999'text': text1000},1001events=events,1002done=done)1003return1004else:1005return TemporaryURL(url=url, ttl=0)10061007file_uuid = self._conn.send_file(filename)10081009mesg = None1010while mesg is None:1011self.message_queue.recv()1012for i, (typ, m) in enumerate(self.message_queue.queue):1013if typ == 'json' and m.get('event') == 'save_blob' and m.get(1014'sha1') == file_uuid:1015mesg = m1016del self.message_queue[i]1017break10181019if 'error' in mesg:1020raise RuntimeError("error saving blob -- %s" % mesg['error'])10211022self._flush_stdio()1023self._send_output(id=self._id,1024once=once,1025file={1026'filename': filename,1027'uuid': file_uuid,1028'show': show,1029'text': text1030},1031events=events,1032done=done)1033if not show:1034info = self.project_info()1035url = "%s/blobs/%s?uuid=%s" % (info['base_url'], filename,1036file_uuid)1037if download:1038url += '?download'1039return TemporaryURL(url=url, ttl=mesg.get('ttl', 0))10401041def python_future_feature(self, feature=None, enable=None):1042"""1043Allow users to enable, disable, and query the features in the python __future__ module.1044"""1045if feature is None:1046if enable is not None:1047raise ValueError(1048"enable may not be specified when feature is None")1049return sorted(Salvus._py_features.keys())10501051attr = getattr(future, feature, None)1052if (feature not in future.all_feature_names) or (1053attr is None) or not isinstance(attr, future._Feature):1054raise RuntimeError("future feature %.50r is not defined" %1055(feature, ))10561057if enable is None:1058return feature in Salvus._py_features10591060if enable:1061Salvus._py_features[feature] = attr1062else:1063try:1064del Salvus._py_features[feature]1065except KeyError:1066pass10671068def default_mode(self, mode=None):1069"""1070Set the default mode for cell evaluation. This is equivalent1071to putting %mode at the top of any cell that does not start1072with %. Use salvus.default_mode() to return the current mode.1073Use salvus.default_mode("") to have no default mode.10741075This is implemented using salvus.cell_prefix.1076"""1077if mode is None:1078return Salvus._default_mode1079Salvus._default_mode = mode1080if mode == "sage":1081self.cell_prefix("")1082else:1083self.cell_prefix("%" + mode)10841085def cell_prefix(self, prefix=None):1086"""1087Make it so that the given prefix code is textually1088prepending to the input before evaluating any cell, unless1089the first character of the cell is a %.10901091To append code at the end, use cell_postfix.10921093INPUT:10941095- ``prefix`` -- None (to return prefix) or a string ("" to disable)10961097EXAMPLES:10981099Make it so every cell is timed:11001101salvus.cell_prefix('%time')11021103Make it so cells are typeset using latex, and latex comments are allowed even1104as the first line.11051106salvus.cell_prefix('%latex')11071108%sage salvus.cell_prefix('')11091110Evaluate each cell using GP (Pari) and display the time it took:11111112salvus.cell_prefix('%time\n%gp')11131114%sage salvus.cell_prefix('') # back to normal1115"""1116if prefix is None:1117return Salvus._prefix1118else:1119Salvus._prefix = prefix11201121def cell_postfix(self, postfix=None):1122"""1123Make it so that the given code is textually1124appended to the input before evaluating a cell.1125To prepend code at the beginning, use cell_prefix.11261127INPUT:11281129- ``postfix`` -- None (to return postfix) or a string ("" to disable)11301131EXAMPLES:11321133Print memory usage after evaluating each cell:11341135salvus.cell_postfix('print("%s MB used"%int(get_memory_usage()))')11361137Return to normal11381139salvus.set_cell_postfix('')11401141"""1142if postfix is None:1143return Salvus._postfix1144else:1145Salvus._postfix = postfix11461147def execute(self, code, namespace=None, preparse=True, locals=None):11481149ascii_warn = False1150code_error = False1151if sys.getdefaultencoding() == 'ascii':1152for c in code:1153if ord(c) >= 128:1154ascii_warn = True1155break11561157if namespace is None:1158namespace = self.namespace11591160# clear pylab figure (takes a few microseconds)1161if pylab is not None:1162pylab.clf()11631164compile_flags = reduce(operator.or_,1165(feature.compiler_flag1166for feature in Salvus._py_features.values()),11670)11681169#code = sage_parsing.strip_leading_prompts(code) # broken -- wrong on "def foo(x):\n print(x)"1170blocks = sage_parsing.divide_into_blocks(code)11711172try:1173import sage.repl1174# CRITICAL -- we do NOT import sage.repl.interpreter!!!!!!!1175# That would waste several seconds importing ipython and much more, which is just dumb.1176# The only reason this is needed below is if the user has run preparser(False), which1177# would cause sage.repl.interpreter to be imported at that point (as preparser is1178# lazy imported.)1179sage_repl_interpreter = sage.repl.interpreter1180except:1181pass # expected behavior usually, since sage.repl.interpreter usually not imported (only used by command line...)11821183import sage.misc.session1184for start, stop, block in blocks:1185# if import sage.repl.interpreter fails, sag_repl_interpreter is unreferenced1186try:1187do_pp = getattr(sage_repl_interpreter, '_do_preparse', True)1188except:1189do_pp = True1190if preparse and do_pp:1191block = sage_parsing.preparse_code(block)1192sys.stdout.reset()1193sys.stderr.reset()1194try:1195b = block.rstrip()1196# get rid of comments at the end of the line -- issue #18351197#from ushlex import shlex1198#s = shlex(b)1199#s.commenters = '#'1200#s.quotes = '"\''1201#b = ''.join(s)1202# e.g. now a line like 'x = test? # bar' becomes 'x=test?'1203if b.endswith('??'):1204p = sage_parsing.introspect(b,1205namespace=namespace,1206preparse=False)1207self.code(source=p['result'], mode="python")1208elif b.endswith('?'):1209p = sage_parsing.introspect(b,1210namespace=namespace,1211preparse=False)1212self.code(source=p['result'], mode="text/x-rst")1213else:1214reload_attached_files_if_mod_smc()1215if execute.count < 2:1216execute.count += 11217if execute.count == 2:1218# this fixup has to happen after first block has executed (os.chdir etc)1219# but before user assigns any variable in worksheet1220# sage.misc.session.init() is not called until first call of show_identifiers1221# BUGFIX: be careful to *NOT* assign to _!! see https://github.com/sagemathinc/cocalc/issues/11071222block2 = "sage.misc.session.state_at_init = dict(globals());sage.misc.session._dummy=sage.misc.session.show_identifiers();\n"1223exec(compile(block2, '', 'single'), namespace,1224locals)1225b2a = """1226if 'SAGE_STARTUP_FILE' in os.environ and os.path.isfile(os.environ['SAGE_STARTUP_FILE']):1227try:1228load(os.environ['SAGE_STARTUP_FILE'])1229except:1230sys.stdout.flush()1231sys.stderr.write('\\nException loading startup file: {}\\n'.format(os.environ['SAGE_STARTUP_FILE']))1232sys.stderr.flush()1233raise1234"""1235exec(compile(b2a, '', 'exec'), namespace, locals)1236features = sage_parsing.get_future_features(1237block, 'single')1238if features:1239compile_flags = reduce(1240operator.or_, (feature.compiler_flag1241for feature in features.values()),1242compile_flags)1243exec(1244compile(block + '\n',1245'',1246'single',1247flags=compile_flags), namespace, locals)1248if features:1249Salvus._py_features.update(features)1250sys.stdout.flush()1251sys.stderr.flush()1252except:1253if ascii_warn:1254sys.stderr.write(1255'\n\n*** WARNING: Code contains non-ascii characters ***\n'1256)1257for c in '\u201c\u201d':1258if c in code:1259sys.stderr.write(1260'*** Maybe the character < %s > should be replaced by < " > ? ***\n'1261% c)1262break1263sys.stderr.write('\n\n')12641265if six.PY2:1266from exceptions import SyntaxError, TypeError1267# py3: all standard errors are available by default via "builtin", not available here for some reason ...1268if six.PY3:1269from builtins import SyntaxError, TypeError12701271exc_type, _, _ = sys.exc_info()1272if exc_type in [SyntaxError, TypeError]:1273from .sage_parsing import strip_string_literals1274code0, _, _ = strip_string_literals(code)1275implicit_mul = RE_POSSIBLE_IMPLICIT_MUL.findall(code0)1276if len(implicit_mul) > 0:1277implicit_mul_list = ', '.join(1278str(_) for _ in implicit_mul)1279# we know there is a SyntaxError and there could be an implicit multiplication1280sys.stderr.write(1281'\n\n*** WARNING: Code contains possible implicit multiplication ***\n'1282)1283sys.stderr.write(1284'*** Check if any of [ %s ] need a "*" sign for multiplication, e.g. 5x should be 5*x ! ***\n\n'1285% implicit_mul_list)12861287sys.stdout.flush()1288sys.stderr.write('Error in lines %s-%s\n' %1289(start + 1, stop + 1))1290traceback.print_exc()1291sys.stderr.flush()1292break12931294def execute_with_code_decorators(self,1295code_decorators,1296code,1297preparse=True,1298namespace=None,1299locals=None):1300"""1301salvus.execute_with_code_decorators is used when evaluating1302code blocks that are set to any non-default code_decorator.1303"""1304import sage # used below as a code decorator1305if is_string(code_decorators):1306code_decorators = [code_decorators]13071308if preparse:1309code_decorators = list(1310map(sage_parsing.preparse_code, code_decorators))13111312code_decorators = [1313eval(code_decorator, self.namespace)1314for code_decorator in code_decorators1315]13161317# The code itself may want to know exactly what code decorators are in effect.1318# For example, r.eval can do extra things when being used as a decorator.1319self.code_decorators = code_decorators13201321for i, code_decorator in enumerate(code_decorators):1322# eval is for backward compatibility1323if not hasattr(code_decorator, 'eval') and hasattr(1324code_decorator, 'before'):1325code_decorators[i] = code_decorator.before(code)13261327for code_decorator in reversed(code_decorators):1328# eval is for backward compatibility1329if hasattr(code_decorator, 'eval'):1330print(code_decorator.eval(1331code, locals=self.namespace)) # removed , end=' '1332code = ''1333elif code_decorator is sage:1334# special case -- the sage module (i.e., %sage) should do nothing.1335pass1336else:1337code = code_decorator(code)1338if code is None:1339code = ''13401341if code != '' and is_string(code):1342self.execute(code,1343preparse=preparse,1344namespace=namespace,1345locals=locals)13461347for code_decorator in code_decorators:1348if not hasattr(code_decorator, 'eval') and hasattr(1349code_decorator, 'after'):1350code_decorator.after(code)13511352def html(self, html, done=False, once=None):1353"""1354Display html in the output stream.13551356EXAMPLE:13571358salvus.html("<b>Hi</b>")1359"""1360self._flush_stdio()1361self._send_output(html=unicode8(html),1362id=self._id,1363done=done,1364once=once)13651366def md(self, md, done=False, once=None):1367"""1368Display markdown in the output stream.13691370EXAMPLE:13711372salvus.md("**Hi**")1373"""1374self._flush_stdio()1375self._send_output(md=unicode8(md), id=self._id, done=done, once=once)13761377def pdf(self, filename, **kwds):1378sage_salvus.show_pdf(filename, **kwds)13791380def tex(self, obj, display=False, done=False, once=None, **kwds):1381"""1382Display obj nicely using TeX rendering.13831384INPUT:13851386- obj -- latex string or object that is automatically be converted to TeX1387- display -- (default: False); if True, typeset as display math (so centered, etc.)1388"""1389self._flush_stdio()1390tex = obj if is_string(obj) else self.namespace['latex'](obj, **kwds)1391self._send_output(tex={1392'tex': tex,1393'display': display1394},1395id=self._id,1396done=done,1397once=once)1398return self13991400def start_executing(self):1401self._send_output(done=False, id=self._id)14021403def clear(self, done=False):1404self._send_output(clear=True, id=self._id, done=done)14051406def delete_last_output(self, done=False):1407self._send_output(delete_last=True, id=self._id, done=done)14081409def stdout(self, output, done=False, once=None):1410"""1411Send the string output (or unicode8(output) if output is not a1412string) to the standard output stream of the compute cell.14131414INPUT:14151416- output -- string or object14171418"""1419stdout = output if is_string(output) else unicode8(output)1420self._send_output(stdout=stdout, done=done, id=self._id, once=once)1421return self14221423def stderr(self, output, done=False, once=None):1424"""1425Send the string output (or unicode8(output) if output is not a1426string) to the standard error stream of the compute cell.14271428INPUT:14291430- output -- string or object14311432"""1433stderr = output if is_string(output) else unicode8(output)1434self._send_output(stderr=stderr, done=done, id=self._id, once=once)1435return self14361437def code(1438self,1439source, # actual source code1440mode=None, # the syntax highlight codemirror mode1441filename=None, # path of file it is contained in (if applicable)1442lineno=-1, # line number where source starts (0-based)1443done=False,1444once=None):1445"""1446Send a code message, which is to be rendered as code by the client, with1447appropriate syntax highlighting, maybe a link to open the source file, etc.1448"""1449source = source if is_string(source) else unicode8(source)1450code = {1451'source': source,1452'filename': filename,1453'lineno': int(lineno),1454'mode': mode1455}1456self._send_output(code=code, done=done, id=self._id, once=once)1457return self14581459def _execute_interact(self, id, vals):1460if id not in sage_salvus.interacts:1461print("(Evaluate this cell to use this interact.)")1462#raise RuntimeError("Error: No interact with id %s"%id)1463else:1464sage_salvus.interacts[id](vals)14651466def interact(self, f, done=False, once=None, **kwds):1467I = sage_salvus.InteractCell(f, **kwds)1468self._flush_stdio()1469self._send_output(interact=I.jsonable(),1470id=self._id,1471done=done,1472once=once)1473return sage_salvus.InteractFunction(I)14741475def javascript(self,1476code,1477once=False,1478coffeescript=False,1479done=False,1480obj=None):1481"""1482Execute the given Javascript code as part of the output1483stream. This same code will be executed (at exactly this1484point in the output stream) every time the worksheet is1485rendered.14861487See the docs for the top-level javascript function for more details.14881489INPUT:14901491- code -- a string1492- once -- boolean (default: FAlse); if True the Javascript is1493only executed once, not every time the cell is loaded. This1494is what you would use if you call salvus.stdout, etc. Use1495once=False, e.g., if you are using javascript to make a DOM1496element draggable (say). WARNING: If once=True, then the1497javascript is likely to get executed before other output to1498a given cell is even rendered.1499- coffeescript -- boolean (default: False); if True, the input1500code is first converted from CoffeeScript to Javascript.15011502At least the following Javascript objects are defined in the1503scope in which the code is evaluated::15041505- cell -- jQuery wrapper around the current compute cell1506- salvus.stdout, salvus.stderr, salvus.html, salvus.tex -- all1507allow you to write additional output to the cell1508- worksheet - jQuery wrapper around the current worksheet DOM object1509- obj -- the optional obj argument, which is passed via JSON serialization1510"""1511if obj is None:1512obj = {}1513self._send_output(javascript={1514'code': code,1515'coffeescript': coffeescript1516},1517id=self._id,1518done=done,1519obj=obj,1520once=once)15211522def coffeescript(self, *args, **kwds):1523"""1524This is the same as salvus.javascript, but with coffeescript=True.15251526See the docs for the top-level javascript function for more details.1527"""1528kwds['coffeescript'] = True1529self.javascript(*args, **kwds)15301531def raw_input(self,1532prompt='',1533default='',1534placeholder='',1535input_width=None,1536label_width=None,1537done=False,1538type=None): # done is ignored here1539self._flush_stdio()1540m = {'prompt': unicode8(prompt)}1541if input_width is not None:1542m['input_width'] = unicode8(input_width)1543if label_width is not None:1544m['label_width'] = unicode8(label_width)1545if default:1546m['value'] = unicode8(default)1547if placeholder:1548m['placeholder'] = unicode8(placeholder)1549self._send_output(raw_input=m, id=self._id)1550typ, mesg = self.message_queue.next_mesg()1551log("handling raw input message ", truncate_text(unicode8(mesg), 400))1552if typ == 'json' and mesg['event'] == 'sage_raw_input':1553# everything worked out perfectly1554self.delete_last_output()1555m['value'] = mesg['value'] # as unicode!1556m['submitted'] = True1557self._send_output(raw_input=m, id=self._id)1558value = mesg['value']1559if type is not None:1560if type == 'sage':1561value = sage_salvus.sage_eval(value)1562else:1563try:1564value = type(value)1565except TypeError:1566# Some things in Sage are clueless about unicode for some reason...1567# Let's at least try, in case the unicode can convert to a string.1568value = type(str(value))1569return value1570else:1571raise KeyboardInterrupt(1572"raw_input interrupted by another action: event='%s' (expected 'sage_raw_input')"1573% mesg['event'])15741575def _check_component(self, component):1576if component not in ['input', 'output']:1577raise ValueError("component must be 'input' or 'output'")15781579def hide(self, component):1580"""1581Hide the given component ('input' or 'output') of the cell.1582"""1583self._check_component(component)1584self._send_output(self._id, hide=component)15851586def show(self, component):1587"""1588Show the given component ('input' or 'output') of the cell.1589"""1590self._check_component(component)1591self._send_output(self._id, show=component)15921593def notify(self, **kwds):1594"""1595Display a graphical notification using the alert_message Javascript function.15961597INPUTS:15981599- `type: "default"` - Type of the notice. "default", "warning", "info", "success", or "error".1600- `title: ""` - The notice's title.1601- `message: ""` - The notice's text.1602- `timeout: ?` - Delay in seconds before the notice is automatically removed.16031604EXAMPLE:16051606salvus.notify(type="warning", title="This warning", message="This is a quick message.", timeout=3)1607"""1608obj = {}1609for k, v in kwds.items():1610if k == 'text': # backward compat1611k = 'message'1612elif k == 'type' and v == 'notice': # backward compat1613v = 'default'1614obj[k] = sage_salvus.jsonable(v)1615if k == 'delay': # backward compat1616obj['timeout'] = v / 1000.0 # units are in seconds now.16171618self.javascript("alert_message(obj)", once=True, obj=obj)16191620def execute_javascript(self, code, coffeescript=False, obj=None):1621"""1622Tell the browser to execute javascript. Basically the same as1623salvus.javascript with once=True (the default), except this1624isn't tied to a particular cell. There is a worksheet object1625defined in the scope of the evaluation.16261627See the docs for the top-level javascript function for more details.1628"""1629self._conn.send_json(1630message.execute_javascript(code,1631coffeescript=coffeescript,1632obj=json.dumps(obj,1633separators=(',', ':'))))16341635def execute_coffeescript(self, *args, **kwds):1636"""1637This is the same as salvus.execute_javascript, but with coffeescript=True.16381639See the docs for the top-level javascript function for more details.1640"""1641kwds['coffeescript'] = True1642self.execute_javascript(*args, **kwds)16431644def _cython(self, filename, **opts):1645"""1646Return module obtained by compiling the Cython code in the1647given file.16481649INPUT:16501651- filename -- name of a Cython file1652- all other options are passed to sage.misc.cython.cython unchanged,1653except for use_cache which defaults to True (instead of False)16541655OUTPUT:16561657- a module1658"""1659if 'use_cache' not in opts:1660opts['use_cache'] = True1661import sage.misc.cython1662modname, path = sage.misc.cython.cython(filename, **opts)1663try:1664sys.path.insert(0, path)1665module = __import__(modname)1666finally:1667del sys.path[0]1668return module16691670def _import_code(self, content, **opts):1671while True:1672py_file_base = uuid().replace('-', '_')1673if not os.path.exists(py_file_base + '.py'):1674break1675try:1676open(py_file_base + '.py', 'w').write(content)1677try:1678sys.path.insert(0, os.path.abspath('.'))1679mod = __import__(py_file_base)1680finally:1681del sys.path[0]1682finally:1683os.unlink(py_file_base + '.py')1684os.unlink(py_file_base + '.pyc')1685return mod16861687def _sage(self, filename, **opts):1688import sage.misc.preparser1689content = "from sage.all import *\n" + sage.misc.preparser.preparse_file(1690open(filename).read())1691return self._import_code(content, **opts)16921693def _spy(self, filename, **opts):1694import sage.misc.preparser1695content = "from sage.all import Integer, RealNumber, PolynomialRing\n" + sage.misc.preparser.preparse_file(1696open(filename).read())1697return self._import_code(content, **opts)16981699def _py(self, filename, **opts):1700return __import__(filename)17011702def require(self, filename, **opts):1703if not os.path.exists(filename):1704raise ValueError("file '%s' must exist" % filename)1705base, ext = os.path.splitext(filename)1706if ext == '.pyx' or ext == '.spyx':1707return self._cython(filename, **opts)1708if ext == ".sage":1709return self._sage(filename, **opts)1710if ext == ".spy":1711return self._spy(filename, **opts)1712if ext == ".py":1713return self._py(filename, **opts)1714raise NotImplementedError("require file of type %s not implemented" %1715ext)17161717def typeset_mode(self, on=True):1718sage_salvus.typeset_mode(on)17191720def project_info(self):1721"""1722Return a dictionary with information about the project in which this code is running.17231724EXAMPLES::17251726sage: salvus.project_info()1727{"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"}1728"""1729return INFO173017311732if six.PY2:1733Salvus.pdf.__func__.__doc__ = sage_salvus.show_pdf.__doc__1734Salvus.raw_input.__func__.__doc__ = sage_salvus.raw_input.__doc__1735Salvus.clear.__func__.__doc__ = sage_salvus.clear.__doc__1736Salvus.delete_last_output.__func__.__doc__ = sage_salvus.delete_last_output.__doc__1737else:1738Salvus.pdf.__doc__ = sage_salvus.show_pdf.__doc__1739Salvus.raw_input.__doc__ = sage_salvus.raw_input.__doc__1740Salvus.clear.__doc__ = sage_salvus.clear.__doc__1741Salvus.delete_last_output.__doc__ = sage_salvus.delete_last_output.__doc__174217431744def execute(conn, id, code, data, cell_id, preparse, message_queue):17451746salvus = Salvus(conn=conn,1747id=id,1748data=data,1749message_queue=message_queue,1750cell_id=cell_id)17511752#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.17531754try:1755# initialize the salvus output streams1756streams = (sys.stdout, sys.stderr)1757sys.stdout = BufferedOutputStream(salvus.stdout)1758sys.stderr = BufferedOutputStream(salvus.stderr)1759try:1760# initialize more salvus functionality1761sage_salvus.set_salvus(salvus)1762namespace['sage_salvus'] = sage_salvus1763except:1764traceback.print_exc()17651766if salvus._prefix:1767if not code.startswith("%"):1768code = salvus._prefix + '\n' + code17691770if salvus._postfix:1771code += '\n' + salvus._postfix17721773salvus.execute(code, namespace=namespace, preparse=preparse)17741775finally:1776# there must be exactly one done message, unless salvus._done is False.1777if sys.stderr._buf:1778if sys.stdout._buf:1779sys.stdout.flush()1780sys.stderr.flush(done=salvus._done)1781else:1782sys.stdout.flush(done=salvus._done)1783(sys.stdout, sys.stderr) = streams178417851786# execute.count goes from 0 to 21787# used for show_identifiers()1788execute.count = 0178917901791def drop_privileges(id, home, transient, username):1792gid = id1793uid = id1794if transient:1795os.chown(home, uid, gid)1796os.setgid(gid)1797os.setuid(uid)1798os.environ['DOT_SAGE'] = home1799mpl = os.environ['MPLCONFIGDIR']1800os.environ['MPLCONFIGDIR'] = home + mpl[5:]1801os.environ['HOME'] = home1802os.environ['IPYTHON_DIR'] = home1803os.environ['USERNAME'] = username1804os.environ['USER'] = username1805os.chdir(home)18061807# Monkey patch the Sage library and anything else that does not1808# deal well with changing user. This sucks, but it is work that1809# simply must be done because we're not importing the library from1810# scratch (which would take a long time).1811import sage.misc.misc1812sage.misc.misc.DOT_SAGE = home + '/.sage/'181318141815class MessageQueue(list):18161817def __init__(self, conn):1818self.queue = []1819self.conn = conn18201821def __repr__(self):1822return "Sage Server Message Queue"18231824def __getitem__(self, i):1825return self.queue[i]18261827def __delitem__(self, i):1828del self.queue[i]18291830def next_mesg(self):1831"""1832Remove oldest message from the queue and return it.1833If the queue is empty, wait for a message to arrive1834and return it (does not place it in the queue).1835"""1836if self.queue:1837return self.queue.pop()1838else:1839return self.conn.recv()18401841def recv(self):1842"""1843Wait until one message is received and enqueue it.1844Also returns the mesg.1845"""1846mesg = self.conn.recv()1847self.queue.insert(0, mesg)1848return mesg184918501851def session(conn):1852"""1853This is run by the child process that is forked off on each new1854connection. It drops privileges, then handles the complete1855compute session.18561857INPUT:18581859- ``conn`` -- the TCP connection1860"""1861mq = MessageQueue(conn)18621863pid = os.getpid()18641865# seed the random number generator(s)1866import sage.all1867sage.all.set_random_seed()1868import random1869random.seed(sage.all.initial_seed())18701871cnt = 01872while True:1873try:1874typ, mesg = mq.next_mesg()18751876#print('INFO:child%s: received message "%s"'%(pid, mesg))1877log("handling message ", truncate_text(unicode8(mesg), 400))1878event = mesg['event']1879if event == 'terminate_session':1880return1881elif event == 'execute_code':1882try:1883execute(conn=conn,1884id=mesg['id'],1885code=mesg['code'],1886data=mesg.get('data', None),1887cell_id=mesg.get('cell_id', None),1888preparse=mesg.get('preparse', True),1889message_queue=mq)1890except Exception as err:1891log("ERROR -- exception raised '%s' when executing '%s'" %1892(err, mesg['code']))1893elif event == 'introspect':1894try:1895# check for introspect from jupyter cell1896prefix = Salvus._default_mode1897if 'top' in mesg:1898top = mesg['top']1899log('introspect cell top line %s' % top)1900if top.startswith("%"):1901prefix = top[1:]1902try:1903# see if prefix is the name of a jupyter kernel function1904kc = eval(prefix + "(get_kernel_client=True)",1905namespace, locals())1906kn = eval(prefix + "(get_kernel_name=True)", namespace,1907locals())1908log("jupyter introspect prefix %s kernel %s" %1909(prefix, kn)) # e.g. "p2", "python2"1910jupyter_introspect(conn=conn,1911id=mesg['id'],1912line=mesg['line'],1913preparse=mesg.get('preparse', True),1914kc=kc)1915except:1916import traceback1917exc_type, exc_value, exc_traceback = sys.exc_info()1918lines = traceback.format_exception(1919exc_type, exc_value, exc_traceback)1920log(lines)1921introspect(conn=conn,1922id=mesg['id'],1923line=mesg['line'],1924preparse=mesg.get('preparse', True))1925except:1926pass1927else:1928raise RuntimeError("invalid message '%s'" % mesg)1929except:1930# When hub connection dies, loop goes crazy.1931# Unfortunately, just catching SIGINT doesn't seem to1932# work, and leads to random exits during a1933# session. Howeer, when connection dies, 10000 iterations1934# happen almost instantly. Ugly, but it works.1935cnt += 11936if cnt > 10000:1937sys.exit(0)1938else:1939pass194019411942def jupyter_introspect(conn, id, line, preparse, kc):1943import jupyter_client1944from queue import Empty19451946try:1947salvus = Salvus(conn=conn, id=id)1948msg_id = kc.complete(line)1949shell = kc.shell_channel1950iopub = kc.iopub_channel19511952# handle iopub responses1953while True:1954try:1955msg = iopub.get_msg(timeout=1)1956msg_type = msg['msg_type']1957content = msg['content']19581959except Empty:1960# shouldn't happen1961log("jupyter iopub channel empty")1962break19631964if msg['parent_header'].get('msg_id') != msg_id:1965continue19661967log("jupyter iopub recv %s %s" % (msg_type, str(content)))19681969if msg_type == 'status' and content['execution_state'] == 'idle':1970break19711972# handle shell responses1973while True:1974try:1975msg = shell.get_msg(timeout=10)1976msg_type = msg['msg_type']1977content = msg['content']19781979except:1980# shouldn't happen1981log("jupyter shell channel empty")1982break19831984if msg['parent_header'].get('msg_id') != msg_id:1985continue19861987log("jupyter shell recv %s %s" % (msg_type, str(content)))19881989if msg_type == 'complete_reply' and content['status'] == 'ok':1990# jupyter kernel returns matches like "xyz.append" and smc wants just "append"1991matches = content['matches']1992offset = content['cursor_end'] - content['cursor_start']1993completions = [s[offset:] for s in matches]1994mesg = message.introspect_completions(id=id,1995completions=completions,1996target=line[-offset:])1997conn.send_json(mesg)1998break1999except:2000log("jupyter completion exception: %s" % sys.exc_info()[0])200120022003def introspect(conn, id, line, preparse):2004salvus = Salvus(2005conn=conn, id=id2006) # so salvus.[tab] works -- note that Salvus(...) modifies namespace.2007z = sage_parsing.introspect(line, namespace=namespace, preparse=preparse)2008if z['get_completions']:2009mesg = message.introspect_completions(id=id,2010completions=z['result'],2011target=z['target'])2012elif z['get_help']:2013mesg = message.introspect_docstring(id=id,2014docstring=z['result'],2015target=z['expr'])2016elif z['get_source']:2017mesg = message.introspect_source_code(id=id,2018source_code=z['result'],2019target=z['expr'])2020conn.send_json(mesg)202120222023def handle_session_term(signum, frame):2024while True:2025try:2026pid, exit_status = os.waitpid(-1, os.WNOHANG)2027except:2028return2029if not pid: return203020312032secret_token = None20332034if 'COCALC_SECRET_TOKEN' in os.environ:2035secret_token_path = os.environ['COCALC_SECRET_TOKEN']2036else:2037secret_token_path = os.path.join(os.environ['SMC'], 'secret_token')203820392040def unlock_conn(conn):2041global secret_token2042if secret_token is None:2043try:2044secret_token = open(secret_token_path).read().strip()2045except:2046conn.send(six.b('n'))2047conn.send(2048six.2049b("Unable to accept connection, since Sage server doesn't yet know the secret token; unable to read from '%s'"2050% secret_token_path))2051conn.close()20522053n = len(secret_token)2054token = six.b('')2055while len(token) < n:2056token += conn.recv(n)2057if token != secret_token[:len(token)]:2058break # definitely not right -- don't try anymore2059if token != six.b(secret_token):2060log("token='%s'; secret_token='%s'" % (token, secret_token))2061conn.send(six.b('n')) # no -- invalid login2062conn.send(six.b("Invalid secret token."))2063conn.close()2064return False2065else:2066conn.send(six.b('y')) # yes -- valid login2067return True206820692070def serve_connection(conn):2071global PID2072PID = os.getpid()2073# First the client *must* send the secret shared token. If they2074# don't, we return (and the connection will have been destroyed by2075# unlock_conn).2076log("Serving a connection")2077log("Waiting for client to unlock the connection...")2078# TODO -- put in a timeout (?)2079if not unlock_conn(conn):2080log("Client failed to unlock connection. Dumping them.")2081return2082log("Connection unlocked.")20832084try:2085conn = ConnectionJSON(conn)2086typ, mesg = conn.recv()2087log("Received message %s" % mesg)2088except Exception as err:2089log("Error receiving message: %s (connection terminated)" % str(err))2090raise20912092if mesg['event'] == 'send_signal':2093if mesg['pid'] == 0:2094log("invalid signal mesg (pid=0)")2095else:2096log("Sending a signal")2097os.kill(mesg['pid'], mesg['signal'])2098return2099if mesg['event'] != 'start_session':2100log("Received an unknown message event = %s; terminating session." %2101mesg['event'])2102return21032104log("Starting a session")2105desc = message.session_description(os.getpid())2106log("child sending session description back: %s" % desc)2107conn.send_json(desc)2108session(conn=conn)210921102111def serve(port, host, extra_imports=False):2112#log.info('opening connection on port %s', port)2113s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)2114s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)21152116# check for children that have finished every few seconds, so2117# we don't end up with zombies.2118s.settimeout(5)21192120s.bind((host, port))2121log('Sage server %s:%s' % (host, port))21222123# Enabling the following signal completely breaks subprocess pexpect in many cases, which is2124# obviously totally unacceptable.2125#signal.signal(signal.SIGCHLD, handle_session_term)21262127def init_library():2128tm = time.time()2129log("pre-importing the sage library...")21302131# FOR testing purposes.2132##log("fake 40 second pause to slow things down for testing....")2133##time.sleep(40)2134##log("done with pause")21352136# Actually import sage now. This must happen after the interact2137# import because of library interacts.2138log("import sage...")2139import sage.all2140log("imported sage.")21412142# Monkey patching interact using the new and improved Salvus2143# implementation of interact.2144sage.all.interact = sage_salvus.interact21452146# Monkey patch the html command.2147try:2148# need the following for sage_server to start with sage-8.02149# or `import sage.interacts.library` will fail (not really important below, as we don't do that).2150import sage.repl.user_globals2151sage.repl.user_globals.set_globals(globals())2152log("initialized user_globals")2153except RuntimeError:2154# may happen with sage version < 8.02155log("user_globals.set_globals failed, continuing", sys.exc_info())21562157sage.all.html = sage.misc.html.html = sage_salvus.html21582159# CRITICAL: look, we are just going to not do this, and have sage.interacts.library2160# be broken. It's **really slow** to do this, and I don't think sage.interacts.library2161# ever ended up going anywhere! People use wiki.sagemath.org/interact instead...2162#import sage.interacts.library2163#sage.interacts.library.html = sage_salvus.html21642165# Set a useful figsize default; the matplotlib one is not notebook friendly.2166import sage.plot.graphics2167sage.plot.graphics.Graphics.SHOW_OPTIONS['figsize'] = [8, 4]21682169# Monkey patch latex.eval, so that %latex works in worksheets2170sage.misc.latex.latex.eval = sage_salvus.latex021712172# Plot, integrate, etc., -- so startup time of worksheets is minimal.2173cmds = [2174'from sage.all import *', 'from sage.calculus.predefined import x',2175'import pylab'2176]2177if extra_imports:2178cmds.extend([2179'import scipy', 'import sympy',2180"plot(sin).save('%s/a.png'%os.environ['SMC'], figsize=2)",2181'integrate(sin(x**2),x)'2182])2183tm0 = time.time()2184for cmd in cmds:2185log(cmd)2186exec(cmd, namespace)21872188global pylab2189pylab = namespace['pylab'] # used for clearing21902191log('imported sage library and other components in %s seconds' %2192(time.time() - tm))21932194for k, v in sage_salvus.interact_functions.items():2195namespace[k] = v2196# See above -- not doing this, since it is REALLY SLOW to import.2197# This does mean that some old code that tries to use interact might break (?).2198#namespace[k] = sagenb.notebook.interact.__dict__[k] = v21992200namespace['_salvus_parsing'] = sage_parsing22012202for name in [2203'anaconda', 'asy', 'attach', 'auto', 'capture', 'cell',2204'clear', 'coffeescript', 'cython', 'default_mode',2205'delete_last_output', 'dynamic', 'exercise', 'fork', 'fortran',2206'go', 'help', 'hide', 'hideall', 'input', 'java', 'javascript',2207'julia', 'jupyter', 'license', 'load', 'md', 'mediawiki',2208'modes', 'octave', 'pandoc', 'perl', 'plot3d_using_matplotlib',2209'prun', 'python_future_feature', 'py3print_mode', 'python',2210'python3', 'r', 'raw_input', 'reset', 'restore', 'ruby',2211'runfile', 'sage_eval', 'scala', 'scala211', 'script',2212'search_doc', 'search_src', 'sh', 'show', 'show_identifiers',2213'singular_kernel', 'time', 'timeit', 'typeset_mode', 'var',2214'wiki'2215]:2216namespace[name] = getattr(sage_salvus, name)22172218namespace['sage_server'] = sys.modules[2219__name__] # http://stackoverflow.com/questions/1676835/python-how-do-i-get-a-reference-to-a-module-inside-the-module-itself22202221# alias pretty_print_default to typeset_mode, since sagenb has/uses that.2222namespace['pretty_print_default'] = namespace['typeset_mode']2223# and monkey patch it2224sage.misc.latex.pretty_print_default = namespace[2225'pretty_print_default']22262227sage_salvus.default_namespace = dict(namespace)2228log("setup namespace with extra functions")22292230# Sage's pretty_print and view are both ancient and a mess2231sage.all.pretty_print = sage.misc.latex.pretty_print = namespace[2232'pretty_print'] = namespace['view'] = namespace['show']22332234# this way client code can tell it is running as a Sage Worksheet.2235namespace['__SAGEWS__'] = True22362237log("Initialize sage library.")2238init_library()22392240t = time.time()2241s.listen(128)2242i = 022432244children = {}2245log("Starting server listening for connections")2246try:2247while True:2248i += 12249#print i, time.time()-t, 'cps: ', int(i/(time.time()-t))2250# do not use log.info(...) in the server loop; threads = race conditions that hang server every so often!!2251try:2252if children:2253for pid in list(children.keys()):2254if os.waitpid(pid, os.WNOHANG) != (0, 0):2255log("subprocess %s terminated, closing connection"2256% pid)2257conn.close()2258del children[pid]22592260try:2261conn, addr = s.accept()2262log("Accepted a connection from", addr)2263except:2264# this will happen periodically since we did s.settimeout above, so2265# that we wait for children above periodically.2266continue2267except socket.error:2268continue2269child_pid = os.fork()2270if child_pid: # parent2271log("forked off child with pid %s to handle this connection" %2272child_pid)2273children[child_pid] = conn2274else:2275# child2276global PID2277PID = os.getpid()2278log("child process, will now serve this new connection")2279serve_connection(conn)22802281# end while2282except Exception as err:2283log("Error taking connection: ", err)2284traceback.print_exc(file=open(LOGFILE, 'a'))2285#log.error("error: %s %s", type(err), str(err))22862287finally:2288log("closing socket")2289#s.shutdown(0)2290s.close()229122922293def run_server(port, host, pidfile, logfile=None):2294global LOGFILE2295if logfile:2296LOGFILE = logfile2297if pidfile:2298pid = str(os.getpid())2299print("os.getpid() = %s" % pid)2300open(pidfile, 'w').write(pid)2301log("run_server: port=%s, host=%s, pidfile='%s', logfile='%s'" %2302(port, host, pidfile, LOGFILE))2303try:2304serve(port, host)2305finally:2306if pidfile:2307os.unlink(pidfile)230823092310if __name__ == "__main__":2311import argparse2312parser = argparse.ArgumentParser(description="Run Sage server")2313parser.add_argument(2314"-p",2315dest="port",2316type=int,2317default=0,2318help=2319"port to listen on (default: 0); 0 = automatically allocated; saved to $SMC/data/sage_server.port"2320)2321parser.add_argument(2322"-l",2323dest='log_level',2324type=str,2325default='INFO',2326help=2327"log level (default: INFO) useful options include WARNING and DEBUG")2328parser.add_argument("-d",2329dest="daemon",2330default=False,2331action="store_const",2332const=True,2333help="daemon mode (default: False)")2334parser.add_argument(2335"--host",2336dest="host",2337type=str,2338default='127.0.0.1',2339help="host interface to bind to -- default is 127.0.0.1")2340parser.add_argument("--pidfile",2341dest="pidfile",2342type=str,2343default='',2344help="store pid in this file")2345parser.add_argument(2346"--logfile",2347dest="logfile",2348type=str,2349default='',2350help="store log in this file (default: '' = don't log to a file)")2351parser.add_argument("-c",2352dest="client",2353default=False,2354action="store_const",2355const=True,2356help="run in test client mode number 1 (command line)")2357parser.add_argument("--hostname",2358dest="hostname",2359type=str,2360default='',2361help="hostname to connect to in client mode")2362parser.add_argument("--portfile",2363dest="portfile",2364type=str,2365default='',2366help="write port to this file")23672368args = parser.parse_args()23692370if args.daemon and not args.pidfile:2371print(("%s: must specify pidfile in daemon mode" % sys.argv[0]))2372sys.exit(1)23732374if args.log_level:2375pass2376#level = getattr(logging, args.log_level.upper())2377#log.setLevel(level)23782379if args.client:2380client1(2381port=args.port if args.port else int(open(args.portfile).read()),2382hostname=args.hostname)2383sys.exit(0)23842385if not args.port:2386s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)2387s.bind(('', 0)) # pick a free port2388args.port = s.getsockname()[1]2389del s23902391if args.portfile:2392open(args.portfile, 'w').write(str(args.port))23932394pidfile = os.path.abspath(args.pidfile) if args.pidfile else ''2395logfile = os.path.abspath(args.logfile) if args.logfile else ''2396if logfile:2397LOGFILE = logfile2398open(LOGFILE, 'w') # for now we clear it on restart...2399log("setting logfile to %s" % LOGFILE)24002401main = lambda: run_server(port=args.port, host=args.host, pidfile=pidfile)2402if args.daemon and args.pidfile:2403from . import daemon2404daemon.daemonize(args.pidfile)2405main()2406else:2407main()240824092410