@cocalc/project
Version:
CoCalc: project daemon
465 lines (431 loc) • 15 kB
JavaScript
// 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