UNPKG

@cocalc/project

Version:
465 lines (431 loc) 15 kB
// Generated by CoffeeScript 2.5.1 (function() { //######################################################################## // This file is part of CoCalc: Copyright © 2020 Sagemath, Inc. // License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details //######################################################################## /* Start the Sage server and also get a new socket connection to it. */ var SAGE_SERVER_MAX_STARTUP_TIME_S, SageSession, _get_sage_socket, _restarted_sage_server, _restarting_sage_server, async, blobs, cache, common, defaults, get_sage_socket, message, misc, misc_node, port_manager, processKill, required, restart_sage_server, secret_token, winston; async = require('async'); winston = require('./logger').getLogger('sage-session'); misc = require('@cocalc/util/misc'); misc_node = require('@cocalc/backend/misc_node'); message = require('@cocalc/util/message'); secret_token = require('./servers/secret-token'); port_manager = require('./port_manager'); common = require('./common'); blobs = require('./blobs'); processKill = require("@cocalc/backend/misc/process-kill").default; ({required, defaults} = misc); //############################################## // Direct Sage socket session -- used internally in local hub, e.g., to assist CodeMirror editors... //############################################## // Wait up to this long for the Sage server to start responding // connection requests, after we restart it. It can // take a while, since it pre-imports the sage library // at startup, before forking. SAGE_SERVER_MAX_STARTUP_TIME_S = 60; _restarting_sage_server = false; _restarted_sage_server = 0; // time when we last restarted it restart_sage_server = function(cb) { var dbg, err, t; dbg = function(m) { return winston.debug(`restart_sage_server: ${misc.to_json(m)}`); }; if (_restarting_sage_server) { dbg("hit lock"); cb("already restarting sage server"); return; } t = new Date() - _restarted_sage_server; if (t <= SAGE_SERVER_MAX_STARTUP_TIME_S * 1000) { err = `restarted sage server ${t}ms ago: not allowing too many restarts too quickly...`; dbg(err); cb(err); return; } _restarting_sage_server = true; dbg("restarting the daemon"); return misc_node.execute_code({ command: "smc-sage-server restart", timeout: 45, ulimit_timeout: false, // very important -- so doesn't kill after 30 seconds of cpu! err_on_exit: true, bash: true, cb: function(err, output) { if (err) { dbg(`failed to restart sage server daemon -- ${err}`); } else { dbg(`successfully restarted sage server daemon -- '${JSON.stringify(output)}'`); } _restarting_sage_server = false; _restarted_sage_server = new Date(); return cb(err); } }); }; // Get a new connection to the Sage server. If the server // isn't running, e.g., it was killed due to running out of memory, // attempt to restart it and try to connect. get_sage_socket = function(cb) { // cb(err, socket) var socket, try_to_connect; socket = void 0; try_to_connect = function(cb) { return _get_sage_socket(function(err, _socket) { if (!err) { socket = _socket; cb(); return; } // Failed for some reason: try to restart one time, then try again. // We do this because the Sage server can easily get killed due to out of memory conditions. // But we don't constantly try to restart the server, since it can easily fail to start if // there is something wrong with a local Sage install. // Note that restarting the sage server doesn't impact currently running worksheets (they // have their own process that isn't killed). return restart_sage_server(function(err) { // won't actually try to restart if called recently. if (err) { cb(err); return; } // success at restarting sage server: *IMMEDIATELY* try to connect return _get_sage_socket(function(err, _socket) { socket = _socket; return cb(err); }); }); }); }; return misc.retry_until_success({ f: try_to_connect, start_delay: 50, max_delay: 5000, factor: 1.5, max_time: SAGE_SERVER_MAX_STARTUP_TIME_S * 1000, log: function(m) { return winston.debug(`get_sage_socket: ${m}`); }, cb: function(err) { return cb(err, socket); } }); }; _get_sage_socket = function(cb) { // cb(err, socket that is ready to use) var port, sage_socket; sage_socket = void 0; port = void 0; return async.series([ (cb) => { winston.debug("get sage server port"); return port_manager.get_port('sage', (err, _port) => { if (err) { cb(err); } else { port = _port; return cb(); } }); }, (cb) => { winston.debug("get and unlock socket"); return misc_node.connect_to_locked_socket({ port: port, token: secret_token.secretToken, cb: (err, _socket) => { if (err) { port_manager.forget_port('sage'); winston.debug(`unlock socket: _new_session: sage session denied connection: ${err}`); cb(`_new_session: sage session denied connection: ${err}`); return; } sage_socket = _socket; winston.debug("Successfully unlocked a sage session connection."); return cb(); } }); }, (cb) => { winston.debug("request sage session from server."); misc_node.enable_mesg(sage_socket); sage_socket.write_mesg('json', message.start_session({ type: 'sage' })); winston.debug("Waiting to read one JSON message back, which will describe the session...."); // TODO: couldn't this just hang forever :-( return sage_socket.once('mesg', (type, desc) => { winston.debug(`Got message back from Sage server: ${common.json(desc)}`); sage_socket.pid = desc.pid; return cb(); }); } ], function(err) { return cb(err, sage_socket); }); }; cache = {}; exports.sage_session = function(opts) { var name; opts = defaults(opts, { client: required, path: required // the path to the *worksheet* file }); // compute and cache if not cached; otherwise, get from cache: return cache[name = opts.path] != null ? cache[name] : cache[name] = new SageSession(opts); }; //# TODO for project-info/server we need a function that returns a path to a sage worksheet for a given PID //exports.get_sage_path = (pid) -> // return path /* Sage Session object Until you actually try to call it no socket need */ SageSession = class SageSession { constructor(opts) { this.dbg = this.dbg.bind(this); this.close = this.close.bind(this); // return true if there is a socket connection to a sage server process this.is_running = this.is_running.bind(this); // NOTE: There can be many simultaneous init_socket calls at the same time, // if e.g., the socket doesn't exist and there are a bunch of calls to @call // at the same time. // See https://github.com/sagemathinc/cocalc/issues/3506 this.init_socket = this.init_socket.bind(this); this._init_path = this._init_path.bind(this); this.call = this.call.bind(this); this._handle_mesg_blob = this._handle_mesg_blob.bind(this); this._handle_mesg_json = this._handle_mesg_json.bind(this); opts = defaults(opts, { client: required, path: required // the path to the *worksheet* file }); this.dbg('constructor')(); this._path = opts.path; this._client = opts.client; this._output_cb = {}; } dbg(f) { return (m) => { return winston.debug(`SageSession(path='${this._path}').${f}: ${m}`); }; } close() { var cb, id, ref, ref1; if (this._socket != null) { processKill(this._socket.pid, 9); } if ((ref = this._socket) != null) { ref.end(); } delete this._socket; ref1 = this._output_cb; for (id in ref1) { cb = ref1[id]; cb({ done: true, error: "killed" }); } this._output_cb = {}; return delete cache[this._path]; } is_running() { return this._socket != null; } init_socket(cb) { var dbg; dbg = this.dbg('init_socket()'); dbg(); if (this._init_socket_cbs != null) { this._init_socket_cbs.push(cb); return; } this._init_socket_cbs = [cb]; return get_sage_socket((err, socket) => { var c, cbs, i, len; if (err) { dbg(`fail -- ${err}.`); cbs = this._init_socket_cbs; delete this._init_socket_cbs; for (i = 0, len = cbs.length; i < len; i++) { c = cbs[i]; c(err); } return; } dbg("successfully opened a sage session"); this._socket = socket; socket.on('end', () => { delete this._socket; return dbg("codemirror session terminated"); }); // CRITICAL: we must define this handler before @_init_path below, // or @_init_path can't possibly work... since it would wait for // this handler to get the response message! socket.on('mesg', (type, mesg) => { var name; dbg(`sage session: received message ${type}`); return typeof this[name = `_handle_mesg_${type}`] === "function" ? this[name](mesg) : void 0; }); return this._init_path((err) => { var j, len1, results; cbs = this._init_socket_cbs; delete this._init_socket_cbs; results = []; for (j = 0, len1 = cbs.length; j < len1; j++) { c = cbs[j]; results.push(c(err)); } return results; }); }); } _init_path(cb) { var dbg, err; dbg = this.dbg("_init_path()"); dbg(); err = void 0; return this.call({ input: { event: 'execute_code', code: "os.chdir(salvus.data['path']);__file__=salvus.data['file']", data: { path: misc_node.abspath(misc.path_split(this._path).head), file: misc_node.abspath(this._path) }, preparse: false }, cb: (resp) => { if (resp.stderr) { err = resp.stderr; dbg(`error '${err}'`); } if (resp.done) { return typeof cb === "function" ? cb(err) : void 0; } } }); } call(opts) { var dbg, ref; opts = defaults(opts, { input: required, cb: void 0 // cb(resp) or cb(resp1), cb(resp2), etc. -- posssibly called multiple times when message is execute or 0 times }); dbg = this.dbg("call"); dbg(`input='${misc.trunc(misc.to_json(opts.input), 300)}'`); switch (opts.input.event) { case 'ping': return typeof opts.cb === "function" ? opts.cb({ pong: true }) : void 0; case 'status': return typeof opts.cb === "function" ? opts.cb({ running: this.is_running() }) : void 0; case 'signal': if (this._socket != null) { dbg(`sending signal ${opts.input.signal} to process ${this._socket.pid}`); processKill(this._socket.pid, opts.input.signal); } return typeof opts.cb === "function" ? opts.cb({}) : void 0; case 'restart': dbg("restarting sage session"); if (this._socket != null) { this.close(); } return this.init_socket((err) => { if (err) { return typeof opts.cb === "function" ? opts.cb({ error: err }) : void 0; } else { return typeof opts.cb === "function" ? opts.cb({}) : void 0; } }); case 'raw_input': dbg("sending sage_raw_input event"); return (ref = this._socket) != null ? ref.write_mesg('json', { event: 'sage_raw_input', value: opts.input.value }) : void 0; default: // send message over socket and get responses return async.series([ (cb) => { if (this._socket != null) { return cb(); } else { return this.init_socket(cb); } }, (cb) => { if (opts.input.id == null) { opts.input.id = misc.uuid(); dbg(`generated new random uuid for input: '${opts.input.id}' `); } this._socket.write_mesg('json', opts.input); if (opts.cb != null) { this._output_cb[opts.input.id] = opts.cb; // this is when opts.cb will get called... } return cb(); } ], (err) => { if (err) { return typeof opts.cb === "function" ? opts.cb({ done: true, error: err }) : void 0; } }); } } _handle_mesg_blob(mesg) { var dbg, uuid; uuid = mesg.uuid; dbg = this.dbg(`_handle_mesg_blob(uuid='${uuid}')`); dbg(); return this._client.save_blob({ blob: mesg.blob, uuid: uuid, cb: (err, resp) => { var ref; if (err) { resp = message.save_blob({ error: err, sha1: uuid // dumb - that sha1 should be called uuid... }); } return (ref = this._socket) != null ? ref.write_mesg('json', resp) : void 0; } }); } _handle_mesg_json(mesg) { var c, dbg; dbg = this.dbg('_handle_mesg_json'); dbg(`mesg='${misc.trunc_middle(misc.to_json(mesg), 400)}'`); c = this._output_cb[mesg != null ? mesg.id : void 0]; if (c != null) { // Must do this check first since it uses done:false. if (mesg.done || (mesg.done == null)) { delete this._output_cb[mesg.id]; mesg.done = true; } if ((mesg.done != null) && !mesg.done) { // waste of space to include done part of mesg if just false for everything else... delete mesg.done; } return c(mesg); } } }; }).call(this); //# sourceMappingURL=sage_session.js.map