UNPKG

smc-hub

Version:

CoCalc: Backend webserver component

1,278 lines (1,213 loc) 45.4 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 //######################################################################## var BLOB_TTL_S, DEBUG, LocalHub, PROJECT_HUB_HEARTBEAT_INTERVAL_S, _local_hub_cache, async, blobs, callback2, clients, connect_to_a_local_hub, defaults, init_server_settings, message, misc, misc_node, required, server_settings, underscore, uuid, winston; ({PROJECT_HUB_HEARTBEAT_INTERVAL_S} = require('smc-util/heartbeat')); // Connection to a Project (="local hub", for historical reasons only.) async = require('async'); ({callback2} = require('smc-util/async-utils')); uuid = require('node-uuid'); winston = require('./logger').getLogger('local-hub-connection'); underscore = require('underscore'); message = require('smc-util/message'); misc_node = require('smc-util-node/misc_node'); misc = require('smc-util/misc'); ({defaults, required} = misc); blobs = require('./blobs'); clients = require('./clients'); // Blobs (e.g., files dynamically appearing as output in worksheets) are kept for this // many seconds before being discarded. If the worksheet is saved (e.g., by a user's autosave), // then the BLOB is saved indefinitely. BLOB_TTL_S = 60 * 60 * 24; // 1 day if (!process.env.SMC_TEST) { DEBUG = true; } connect_to_a_local_hub = function(opts) { // opts.cb(err, socket) opts = defaults(opts, { port: required, host: required, secret_token: required, timeout: 10, cb: required }); return misc_node.connect_to_locked_socket({ port: opts.port, host: opts.host, token: opts.secret_token, timeout: opts.timeout, cb: (err, socket) => { if (err) { return opts.cb(err); } else { misc_node.enable_mesg(socket, 'connection_to_a_local_hub'); socket.on('data', function(data) { return misc_node.keep_portforward_alive(opts.port); }); return opts.cb(void 0, socket); } } }); }; _local_hub_cache = {}; exports.new_local_hub = function(project_id, database, compute_server) { var H; if (project_id == null) { throw "project_id must be specified (it is undefined)"; } H = _local_hub_cache[project_id]; if (H != null) { winston.debug(`new_local_hub('${project_id}') -- using cached version`); } else { winston.debug(`new_local_hub('${project_id}') -- creating new one`); H = new LocalHub(project_id, database, compute_server); _local_hub_cache[project_id] = H; } return H; }; exports.connect_to_project = function(project_id, database, compute_server, cb) { var hub; hub = exports.new_local_hub(project_id, database, compute_server); return hub.local_hub_socket(function(err) { if (err) { winston.debug(`connect_to_project: error ensuring connection to ${project_id} -- ${err}`); } else { winston.debug(`connect_to_project: successfully ensured connection to ${project_id}`); } return typeof cb === "function" ? cb(err) : void 0; }); }; exports.disconnect_from_project = function(project_id) { var H; H = _local_hub_cache[project_id]; delete _local_hub_cache[project_id]; if (H != null) { H.free_resources(); } }; exports.all_local_hubs = function() { var h, k, v; v = []; for (k in _local_hub_cache) { h = _local_hub_cache[k]; if (h != null) { v.push(h); } } return v; }; server_settings = void 0; init_server_settings = async function() { var update; server_settings = (await require('./servers/server-settings').default()); update = function() { var i, len, ref, results1, x; winston.debug("local_hub_connection (version might have changed) -- checking on clients"); ref = exports.all_local_hubs(); results1 = []; for (i = 0, len = ref.length; i < len; i++) { x = ref[i]; results1.push(x.restart_if_version_too_old()); } return results1; }; update(); return server_settings.table.on('change', update); }; LocalHub = class LocalHub { // use the function "new_local_hub" above; do not construct this directly! constructor(project_id1, database1, compute_server1) { this.init_heartbeat = this.init_heartbeat.bind(this); this.delete_heartbeat = this.delete_heartbeat.bind(this); this.project = this.project.bind(this); this.dbg = this.dbg.bind(this); this.restart = this.restart.bind(this); this.status = this.status.bind(this); this.state = this.state.bind(this); this.free_resources = this.free_resources.bind(this); this.free_resources_for_client_id = this.free_resources_for_client_id.bind(this); // async this.init_ephemeral = this.init_ephemeral.bind(this); this.ephemeral_disk = this.ephemeral_disk.bind(this); this.ephemeral_state = this.ephemeral_state.bind(this); // Project query support code this.mesg_query = this.mesg_query.bind(this); this.mesg_query_cancel = this.mesg_query_cancel.bind(this); this.query_cancel_all_changefeeds = this.query_cancel_all_changefeeds.bind(this); // async -- throws error if project doesn't have access to string with this id. this.check_syncdoc_access = this.check_syncdoc_access.bind(this); this.mesg_get_syncdoc_history = this.mesg_get_syncdoc_history.bind(this); // end project query support code // local hub just told us its version. Record it. Restart project if hub version too old. this.local_hub_version = this.local_hub_version.bind(this); // If our known version of the project is too old compared to the // current version_min_project in smcu-util/smc-version, then // we restart the project, which updates the code to the latest // version. Only restarts the project if we have an open control // socket to it. // Please make damn sure to update the project code on the compute // server before updating the version, or the project will be // forced to restart and it won't help! this.restart_if_version_too_old = this.restart_if_version_too_old.bind(this); // handle incoming JSON messages from the local_hub this.handle_mesg = this.handle_mesg.bind(this); this.handle_blob = this.handle_blob.bind(this); // Connection to the remote local_hub daemon that we use for control. this.local_hub_socket = this.local_hub_socket.bind(this); // Get a new connection to the local_hub, // authenticated via the secret_token, and enhanced // to be able to send/receive json and blob messages. this.new_socket = this.new_socket.bind(this); this.remove_multi_response_listener = this.remove_multi_response_listener.bind(this); this.call = this.call.bind(this); // As mentioned above -- there's no else -- if not timeout then // we do not listen for a response. //################################################### // Session management //#################################################### this._open_session_socket = this._open_session_socket.bind(this); // Connect the client with a console session, possibly creating a session in the process. this.console_session = this.console_session.bind(this); this.terminate_session = this.terminate_session.bind(this); // Read a file from a project into memory on the hub. // I think this is used only by the API, but not by browser clients anymore. this.read_file = this.read_file.bind(this); // Write a file to a project // I think this is used only by the API, but not by browser clients anymore. this.write_file = this.write_file.bind(this); this.project_id = project_id1; this.database = database1; this.compute_server = compute_server1; if (server_settings == null) { // module being used -- make sure server_settings is initialized init_server_settings(); } this._local_hub_socket_connecting = false; this._sockets = {}; // key = session_uuid:client_id this._sockets_by_client_id = {}; //key = client_id, value = list of sockets for that client this.call_callbacks = {}; this.path = '.'; // should deprecate - *is* used by some random code elsewhere in this file this.dbg("getting deployed running project"); } init_heartbeat() { var send_heartbeat; this.dbg("init_heartbeat"); if (this._heartbeat_interval != null) { this.dbg("init_heartbeat -- already running"); return; // already running } send_heartbeat = () => { var ref; this.dbg("init_heartbeat -- send"); return (ref = this._socket) != null ? ref.write_mesg('json', message.heartbeat()) : void 0; }; return this._heartbeat_interval = setInterval(send_heartbeat, PROJECT_HUB_HEARTBEAT_INTERVAL_S * 1000); } delete_heartbeat() { if (this._heartbeat_interval != null) { this.dbg("delete_heartbeat"); clearInterval(this._heartbeat_interval); return delete this._heartbeat_interval; } } async project(cb) { var err; try { return cb(void 0, (await this.compute_server(this.project_id))); } catch (error) { err = error; return cb(err); } } dbg(m) { //# only enable when debugging if (DEBUG) { return winston.debug(`local_hub('${this.project_id}'): ${misc.to_json(m)}`); } } async restart(cb) { var err; this.dbg("restart"); this.free_resources(); try { await ((await this.compute_server(this.project_id))).restart(); return cb(); } catch (error) { err = error; return cb(err); } } async status(cb) { var err; this.dbg("status: get status of a project"); try { return cb(void 0, (await ((await this.compute_server(this.project_id))).status())); } catch (error) { err = error; return cb(err); } } async state(cb) { var err; this.dbg("state: get state of a project"); try { return cb(void 0, (await ((await this.compute_server(this.project_id))).state())); } catch (error) { err = error; return cb(err); } } free_resources() { var e, k, ref, ref1, s; this.dbg("free_resources"); this.query_cancel_all_changefeeds(); this.delete_heartbeat(); delete this._ephemeral; if (this._ephemeral_timeout) { clearTimeout(this._ephemeral_timeout); delete this._ephemeral_timeout; } delete this.address; // so we don't continue trying to use old address delete this._status; delete this.smc_version; // so when client next connects we ignore version checks until they tell us their version try { if ((ref = this._socket) != null) { ref.end(); } winston.debug("free_resources: closed main local_hub socket"); } catch (error) { e = error; winston.debug(`free_resources: exception closing main _socket: ${e}`); } delete this._socket; ref1 = this._sockets; for (k in ref1) { s = ref1[k]; try { s.end(); winston.debug(`free_resources: closed ${k}`); } catch (error) { e = error; winston.debug(`free_resources: exception closing a socket: ${e}`); } } this._sockets = {}; return this._sockets_by_client_id = {}; } free_resources_for_client_id(client_id) { var e, i, len, socket, v; v = this._sockets_by_client_id[client_id]; if (v != null) { this.dbg(`free_resources_for_client_id(${client_id}) -- ${v.length} sockets`); for (i = 0, len = v.length; i < len; i++) { socket = v[i]; try { socket.end(); socket.destroy(); } catch (error) { e = error; } } // do nothing return delete this._sockets_by_client_id[client_id]; } } async init_ephemeral() { var settings; settings = (await callback2(this.database.get_project_settings, { project_id: this.project_id })); this._ephemeral = misc.copy_with(settings, ['ephemeral_disk', 'ephemeral_state']); this.dbg(`init_ephemeral -- ${JSON.stringify(this._ephemeral)}`); // cache for 60s return this._ephemeral_timeout = setTimeout((() => { return delete this._ephemeral; }), 60000); } async ephemeral_disk() { if (this._ephemeral == null) { await this.init_ephemeral(); } return this._ephemeral.ephemeral_disk; } async ephemeral_state() { if (this._ephemeral == null) { await this.init_ephemeral(); } return this._ephemeral.ephemeral_state; } async mesg_query(mesg, write_mesg) { var dbg, first, mesg_id, query; dbg = (m) => { return winston.debug(`mesg_query(project_id='${this.project_id}'): ${misc.trunc(m, 200)}`); }; dbg(misc.to_json(mesg)); query = mesg.query; if (query == null) { write_mesg(message.error({ error: "query must be defined" })); return; } if ((await this.ephemeral_state())) { this.dbg("project has ephemeral state"); write_mesg(message.error({ error: "FATAL -- project has ephemeral state so no database queries are allowed" })); return; } this.dbg("project does NOT have ephemeral state"); first = true; if (mesg.changes) { if (this._query_changefeeds == null) { this._query_changefeeds = {}; } this._query_changefeeds[mesg.id] = true; } mesg_id = mesg.id; return this.database.user_query({ project_id: this.project_id, query: query, options: mesg.options, changes: mesg.changes ? mesg_id : void 0, cb: (err, result) => { var ref, resp; if ((result != null ? result.action : void 0) === 'close') { err = 'close'; } if (err) { dbg(`project_query error: ${misc.to_json(err)}`); if ((ref = this._query_changefeeds) != null ? ref[mesg_id] : void 0) { delete this._query_changefeeds[mesg_id]; } write_mesg(message.error({ error: err })); if (mesg.changes && !first) { // also, assume changefeed got messed up, so cancel it. return this.database.user_query_cancel_changefeed({ id: mesg_id }); } } else { //if Math.random() <= .3 # for testing -- force forgetting about changefeed with probability 10%. // delete @_query_changefeeds[mesg_id] if (mesg.changes && !first) { resp = result; resp.id = mesg_id; resp.multi_response = true; } else { first = false; resp = mesg; resp.query = result; } return write_mesg(resp); } } }); } mesg_query_cancel(mesg, write_mesg) { if (this._query_changefeeds == null) { // no changefeeds return write_mesg(mesg); } else { return this.database.user_query_cancel_changefeed({ id: mesg.id, cb: (err, resp) => { var ref; if (err) { return write_mesg(message.error({ error: err })); } else { mesg.resp = resp; write_mesg(mesg); return (ref = this._query_changefeeds) != null ? delete ref[mesg.id] : void 0; } } }); } } query_cancel_all_changefeeds(cb) { var dbg, f, v; if ((this._query_changefeeds == null) || this._query_changefeeds.length === 0) { if (typeof cb === "function") { cb(); } return; } dbg = (m) => { return winston.debug(`query_cancel_all_changefeeds(project_id='${this.project_id}'): ${m}`); }; v = this._query_changefeeds; dbg(`canceling ${v.length} changefeeds`); delete this._query_changefeeds; f = (id, cb) => { dbg(`canceling id=${id}`); return this.database.user_query_cancel_changefeed({ id: id, cb: (err) => { if (err) { dbg(`FEED: warning ${id} -- error canceling a changefeed ${misc.to_json(err)}`); } else { dbg(`FEED: canceled changefeed -- ${id}`); } return cb(); } }); }; return async.map(misc.keys(v), f, (err) => { return typeof cb === "function" ? cb(err) : void 0; }); } async check_syncdoc_access(string_id) { var opts, results; if (!typeof string_id === 'string' && string_id.length === 40) { throw Error('string_id must be specified and valid'); return; } opts = { query: "SELECT project_id FROM syncstrings", where: { "string_id = $::CHAR(40)": string_id } }; results = (await callback2(this.database._query, opts)); if (results.rows.length !== 1) { throw Error("no such syncdoc"); } if (results.rows[0].project_id !== this.project_id) { throw Error("project does NOT have access to this syncdoc"); // everything is fine. } } async mesg_get_syncdoc_history(mesg, write_mesg) { var err, history; try { // this raises an error if user does not have access await this.check_syncdoc_access(mesg.string_id); // get the history history = (await this.database.syncdoc_history_async(mesg.string_id, mesg.patches)); return write_mesg(message.syncdoc_history({ id: mesg.id, history: history })); } catch (error) { err = error; return write_mesg(message.error({ id: mesg.id, error: `unable to get syncdoc history for string_id ${mesg.string_id} -- ${err}` })); } } local_hub_version(version) { winston.debug(`local_hub_version: version=${version}`); this.smc_version = version; return this.restart_if_version_too_old(); } restart_if_version_too_old() { var f, ver; if (this._socket == null) { return; } // not connected at all -- just return if (this.smc_version == null) { return; } // client hasn't told us their version yet if (server_settings.version.version_min_project <= this.smc_version) { return; } // the project is up to date if (this._restart_goal_version === server_settings.version.version_min_project) { return; } // We already restarted the project in an attempt to update it to this version // and it didn't get updated. Don't try again until @_restart_version is cleared, since // we don't want to lock a user out of their project due to somebody forgetting // to update code on the compute server! It could also be that the project just // didn't finish restarting. winston.debug(`restart_if_version_too_old(${this.project_id}): ${this.smc_version}, ${server_settings.version.version_min_project}`); // record some stuff so that we don't keep trying to restart the project constantly ver = this._restart_goal_version = server_settings.version.version_min_project; // version which we tried to get to f = () => { if (this._restart_goal_version === ver) { return delete this._restart_goal_version; } }; setTimeout(f, 15 * 60 * 1000); // don't try again for at least 15 minutes. this.dbg(`restart_if_version_too_old -- restarting since ${server_settings.version.version_min_project} > ${this.smc_version}`); return this.restart((err) => { return this.dbg(`restart_if_version_too_old -- done ${err}`); }); } handle_mesg(mesg, socket) { var f, write_mesg; this.dbg(`local_hub --> hub: received mesg: ${misc.trunc(misc.to_json(mesg), 250)}`); if (mesg.client_id != null) { // Should we worry about ensuring that message from this local hub are allowed to // send messages to this client? NO. For them to send a message, they would have to // know the client's id, which is a random uuid, assigned each time the user connects. // It obviously is known to the local hub -- but if the user has connected to the local // hub then they should be allowed to receive messages. clients.pushToClient(mesg); return; } if (mesg.event === 'version') { this.local_hub_version(mesg.version); return; } if (mesg.id != null) { f = this.call_callbacks[mesg.id]; if (f != null) { f(mesg); } else { winston.debug("handling call from local_hub"); write_mesg = (resp) => { resp.id = mesg.id; return this.local_hub_socket((err, sock) => { if (!err) { return sock.write_mesg('json', resp); } }); }; switch (mesg.event) { case 'ping': write_mesg(message.pong()); break; case 'query': this.mesg_query(mesg, write_mesg); break; case 'query_cancel': this.mesg_query_cancel(mesg, write_mesg); break; case 'get_syncdoc_history': this.mesg_get_syncdoc_history(mesg, write_mesg); break; case 'file_written_to_project': return; // ignore -- don't care; this is going away case 'file_read_from_project': return; // handle elsewhere by the code that requests the file case 'error': return; default: // ignore -- don't care since handler already gone. write_mesg(message.error({ error: `unknown event '${mesg.event}'` })); } } } } handle_blob(opts) { opts = defaults(opts, { uuid: required, blob: required }); this.dbg(`local_hub --> global_hub: received a blob with uuid ${opts.uuid}`); // Store blob in DB. return blobs.save_blob({ uuid: opts.uuid, blob: opts.blob, project_id: this.project_id, ttl: BLOB_TTL_S, check: true, // if malicious user tries to overwrite a blob with given sha1 hash, they get an error. database: this.database, cb: (err, ttl) => { var resp; if (err) { resp = message.save_blob({ sha1: opts.uuid, error: err }); this.dbg(`handle_blob: error! -- ${err}`); } else { resp = message.save_blob({ sha1: opts.uuid, ttl: ttl }); } return this.local_hub_socket((err, socket) => { if (!err) { return socket.write_mesg('json', resp); } }); } }); } local_hub_socket(cb) { var cancel_connecting, connecting_timer; if (this._socket != null) { //@dbg("local_hub_socket: re-using existing socket") cb(void 0, this._socket); return; } if (this._local_hub_socket_connecting) { this._local_hub_socket_queue.push(cb); this.dbg(`local_hub_socket: added socket request to existing queue, which now has length ${this._local_hub_socket_queue.length}`); return; } this._local_hub_socket_connecting = true; this._local_hub_socket_queue = [cb]; connecting_timer = void 0; cancel_connecting = () => { var c, i, len, ref; this._local_hub_socket_connecting = false; if (this._local_hub_socket_queue != null) { this.dbg("local_hub_socket: cancelled due to timeout"); ref = this._local_hub_socket_queue; for (i = 0, len = ref.length; i < len; i++) { c = ref[i]; if (typeof c === "function") { c('timeout'); } } delete this._local_hub_socket_queue; } return clearTimeout(connecting_timer); }; // If below fails for 20s for some reason, cancel everything to allow for future attempt. connecting_timer = setTimeout(cancel_connecting, 20000); this.dbg("local_hub_socket: getting new socket"); return this.new_socket((err, socket) => { var c, check_version_received, i, j, len, len1, ref, ref1; if (this._local_hub_socket_queue == null) { return; } // already gave up. this._local_hub_socket_connecting = false; this.dbg(`local_hub_socket: new_socket returned ${err}`); if (err) { ref = this._local_hub_socket_queue; for (i = 0, len = ref.length; i < len; i++) { c = ref[i]; if (typeof c === "function") { c(err); } } delete this._local_hub_socket_queue; } else { socket.on('mesg', (type, mesg) => { switch (type) { case 'blob': return this.handle_blob(mesg); case 'json': return this.handle_mesg(mesg, socket); } }); socket.on('end', this.free_resources); socket.on('close', this.free_resources); socket.on('error', this.free_resources); // Send a hello message to the local hub, so it knows this is the control connection, // and not something else (e.g., a console). socket.write_mesg('json', { event: 'hello' }); ref1 = this._local_hub_socket_queue; for (j = 0, len1 = ref1.length; j < len1; j++) { c = ref1[j]; if (typeof c === "function") { c(void 0, socket); } } delete this._local_hub_socket_queue; this._socket = socket; this.init_heartbeat(); // start sending heartbeat over this socket // Finally, we wait a bit to see if the version gets sent from // the client. If not, we set it to 0, which will cause a restart, // which will upgrade to a new version that sends versions. // TODO: This code can be deleted after all projects get restarted. check_version_received = () => { if ((this._socket != null) && (this.smc_version == null)) { this.smc_version = 0; return this.restart_if_version_too_old(); } }; setTimeout(check_version_received, 60 * 1000); } return cancel_connecting(); }); } new_socket(cb) { // cb(err, socket) var f, socket; this.dbg("new_socket"); f = (cb) => { var ref; if (this.address == null) { cb("no address"); return; } if (this.address.port == null) { cb("no port"); return; } if (this.address.host == null) { cb("no host"); return; } if (this.address.secret_token == null) { cb("no secret_token"); return; } return connect_to_a_local_hub({ port: this.address.port, host: (ref = this.address.ip) != null ? ref : this.address.host, // prefer @address.ip if it exists (e.g., for cocalc-kubernetes); otherwise use host (which is where compute server is). secret_token: this.address.secret_token, cb: cb }); }; socket = void 0; return async.series([ async(cb) => { var err; if (this.address == null) { this.dbg("get address of a working local hub"); try { this.address = (await ((await this.compute_server(this.project_id))).address()); return cb(); } catch (error) { err = error; return cb(err); } } else { return cb(); } }, (cb) => { this.dbg("try to connect to local hub socket using last known address"); return f(async(err, _socket) => { if (!err) { socket = _socket; return cb(); } else { this.dbg(`failed to get address of a working local hub -- ${err}`); try { this.address = (await ((await this.compute_server(this.project_id))).address()); return cb(); } catch (error) { err = error; return cb(err); } } }); }, (cb) => { if (socket == null) { this.dbg("still don't have our connection -- try again"); return f((err, _socket) => { socket = _socket; return cb(err); }); } else { return cb(); } } ], (err) => { return cb(err, socket); }); } remove_multi_response_listener(id) { return delete this.call_callbacks[id]; } call(opts) { opts = defaults(opts, { mesg: required, timeout: void 0, // NOTE: a nonzero timeout MUST be specified, or we will not even listen for a response from the local hub! (Ensures leaking listeners won't happen.) multi_response: false, // if true, timeout ignored; call @remove_multi_response_listener(mesg.id) to remove cb: void 0 }); this.dbg("call"); if (opts.mesg.id == null) { if (opts.timeout || opts.multi_response) { // opts.timeout being undefined or 0 both mean "don't do it" opts.mesg.id = uuid.v4(); } } return this.local_hub_socket((err, socket) => { if (err) { this.dbg(`call: failed to get socket -- ${err}`); if (typeof opts.cb === "function") { opts.cb(err); } return; } this.dbg(`call: get socket -- now writing message to the socket -- ${misc.trunc(misc.to_json(opts.mesg), 200)}`); return socket.write_mesg('json', opts.mesg, (err) => { if (err) { this.free_resources(); // at least next time it will get a new socket if (typeof opts.cb === "function") { opts.cb(err); } return; } if (opts.multi_response) { return this.call_callbacks[opts.mesg.id] = opts.cb; } else if (opts.timeout) { // Listen to exactly one response, them remove the listener: return this.call_callbacks[opts.mesg.id] = (resp) => { delete this.call_callbacks[opts.mesg.id]; if (resp.event === 'error') { return opts.cb(resp.error); } else { return opts.cb(void 0, resp); } }; } }); }); } _open_session_socket(opts) { var key, socket; opts = defaults(opts, { client_id: required, session_uuid: required, type: required, // 'sage', 'console' params: required, project_id: required, timeout: 10, cb: required // cb(err, socket) }); this.dbg("_open_session_socket"); // We do not currently have an active open socket connection to this session. // We make a new socket connection to the local_hub, then // send a connect_to_session message, which will either // plug this socket into an existing session with the given session_uuid, or // create a new session with that uuid and plug this socket into it. key = `${opts.session_uuid}:${opts.client_id}`; socket = this._sockets[key]; if (socket != null) { opts.cb(false, socket); return; } socket = void 0; return async.series([ (cb) => { this.dbg("_open_session_socket: getting new socket connection to a local_hub"); return this.new_socket((err, _socket) => { if (err) { return cb(err); } else { socket = _socket; socket._key = key; this._sockets[key] = socket; if (this._sockets_by_client_id[opts.client_id] == null) { this._sockets_by_client_id[opts.client_id] = [socket]; } else { this._sockets_by_client_id[opts.client_id].push(socket); } return cb(); } }); }, (cb) => { var f, mesg, timed_out, timer; mesg = message.connect_to_session({ id: uuid.v4(), // message id type: opts.type, project_id: opts.project_id, session_uuid: opts.session_uuid, params: opts.params }); this.dbg(`_open_session_socket: send the message asking to be connected with a ${opts.type} session.`); socket.write_mesg('json', mesg); // Now we wait for a response for opt.timeout seconds f = (type, resp) => { clearTimeout(timer); //@dbg("Getting #{opts.type} session -- get back response type=#{type}, resp=#{misc.to_json(resp)}") if (resp.event === 'error') { return cb(resp.error); } else { if (opts.type === 'console') { // record the history, truncating in case the local_hub sent something really long (?) if (resp.history != null) { socket.history = resp.history.slice(resp.history.length - 100000); } else { socket.history = ''; } // Console -- we will now only use this socket for binary communications. misc_node.disable_mesg(socket); } return cb(); } }; socket.once('mesg', f); timed_out = () => { socket.removeListener('mesg', f); socket.end(); return cb(`Timed out after waiting ${opts.timeout} seconds for response from ${opts.type} session server. Please try again later.`); }; return timer = setTimeout(timed_out, opts.timeout * 1000); } ], (err) => { var ref; if (err) { this.dbg(`_open_session_socket: error getting a socket -- (declaring total disaster) -- ${err}`); // This @_socket.destroy() below is VERY important, since just deleting the socket might not send this, // and the local_hub -- if the connection were still good -- would have two connections // with the global hub, thus doubling sync and broadcast messages. NOT GOOD. if ((ref = this._socket) != null) { ref.destroy(); } delete this._status; return delete this._socket; } else if (socket != null) { return opts.cb(false, socket); } }); } console_session(opts) { opts = defaults(opts, { client: required, project_id: required, params: required, session_uuid: void 0, // if undefined, a new session is created; if defined, connect to session or get error cb: required // cb(err, [session_connected message]) }); this.dbg(`console_session: connect client to console session -- session_uuid=${opts.session_uuid}`); // Connect to the console server if (opts.session_uuid == null) { // Create a new session opts.session_uuid = uuid.v4(); } return this._open_session_socket({ client_id: opts.client.id, session_uuid: opts.session_uuid, project_id: opts.project_id, type: 'console', params: opts.params, cb: (err, console_socket) => { var channel, f, mesg, recently_sent_reconnect; if (err) { opts.cb(err); return; } // In case it was already setup to listen before... (and client is reconnecting) console_socket.removeAllListeners(); console_socket._ignore = false; console_socket.on('end', () => { winston.debug(`console_socket (session_uuid=${opts.session_uuid}): received 'end' so setting ignore=true`); opts.client.push_to_client(message.terminate_session({ session_uuid: opts.session_uuid })); console_socket._ignore = true; return delete this._sockets[console_socket._key]; }); // Plug the two consoles together // client --> console: // Create a binary channel that the client can use to write to the socket. // (This uses our system for multiplexing JSON and multiple binary streams // over one single connection.) recently_sent_reconnect = false; //winston.debug("installing data handler -- ignore='#{console_socket._ignore}") channel = opts.client.register_data_handler((data) => { //winston.debug("handling data -- ignore='#{console_socket._ignore}'; path='#{opts.path}'") if (!console_socket._ignore) { try { console_socket.write(data); } catch (error) { } /* Sometimes this appears in the server logs, where local_hub_connection.coffee:719 below was the above line: 2017-06-16 12:03:46.749111 | 04:33:22.604308 | uncaught_exception | {"host": "smc-hub-242825805-9pkmp", "error": "Error: write after end", "stack": "Error: write after end\n at writeAfterEnd (_stream_writable.js:193:12)\n at Socket.Writable.write (_stream_writable.js:240:5)\n at Socket.write (net.js:657:40)\n at Array.<anonymous> (/cocalc/src/smc-hub/local_hub_connection.coffee:719:40)\n at Timeout._onTimeout (/cocalc/src/smc-hub/client.coffee:540:13)\n at ontimeout (timers.js:386:14)\n at tryOnTimeout (timers.js:250:5)\n at Timer.listOnTimeout (timers.js:214:5)\n"} */ if (opts.params.filename != null) { return opts.client.touch({ project_id: opts.project_id, path: opts.params.filename }); } } else { // send a reconnect message, but at most once every 5 seconds. if (!recently_sent_reconnect) { recently_sent_reconnect = true; setTimeout((() => { return recently_sent_reconnect = false; }), 5000); winston.debug(`console -- trying to write to closed console_socket with session_uuid=${opts.session_uuid}`); return opts.client.push_to_client(message.session_reconnect({ session_uuid: opts.session_uuid })); } } }); mesg = message.session_connected({ session_uuid: opts.session_uuid, data_channel: channel, history: console_socket.history }); opts.cb(false, mesg); // console --> client: // When data comes in from the socket, we push it on to the connected // client over the channel we just created. f = function(data) { // Never push more than 20000 characters at once to client, since display is slow, etc. if (data.length > 20000) { data = "[...]" + data.slice(data.length - 20000); } //winston.debug("push_data_to_client('#{data}')") opts.client.push_data_to_client(channel, data); console_socket.history += data; if (console_socket.history.length > 150000) { return console_socket.history = console_socket.history.slice(console_socket.history.length - 100000); } }; return console_socket.on('data', f); } }); } terminate_session(opts) { opts = defaults(opts, { session_uuid: required, project_id: required, cb: void 0 }); this.dbg("terminate_session"); return this.call({ mesg: message.terminate_session({ session_uuid: opts.session_uuid, project_id: opts.project_id }), timeout: 30, cb: opts.cb }); } read_file(opts) { // cb(err, content_of_file) var archive, cb, data, data_uuid, id, path, project_id, result_archive, socket; ({path, project_id, archive, cb} = defaults(opts, { path: required, project_id: required, archive: 'tar.bz2', // for directories; if directory, then the output object "data" has data.archive=actual extension used. cb: required })); this.dbg(`read_file '${path}'`); socket = void 0; id = uuid.v4(); data = void 0; data_uuid = void 0; result_archive = void 0; return async.series([ // Get a socket connection to the local_hub. (cb) => { return this.local_hub_socket((err, _socket) => { if (err) { return cb(err); } else { socket = _socket; return cb(); } }); }, (cb) => { socket.write_mesg('json', message.read_file_from_project({ id: id, project_id: project_id, path: path, archive: archive })); return socket.recv_mesg({ type: 'json', id: id, timeout: 60, cb: (mesg) => { switch (mesg.event) { case 'error': return cb(mesg.error); case 'file_read_from_project': data_uuid = mesg.data_uuid; result_archive = mesg.archive; return cb(); default: return cb(`Unknown mesg event '${mesg.event}'`); } } }); }, (cb) => { return socket.recv_mesg({ type: 'blob', id: data_uuid, timeout: 60, cb: (_data) => { // recv_mesg returns either a Buffer blob // *or* a {event:'error', error:'the error'} object. // Fortunately `new Buffer().event` is valid (and undefined). if (_data.event === 'error') { return cb(_data.error); } else { data = _data; data.archive = result_archive; return cb(); } } }); } ], (err) => { if (err) { return cb(err); } else { return cb(void 0, data); } }); } write_file(opts) { // cb(err) var cb, data, data_uuid, id, path, project_id; ({path, project_id, cb, data} = defaults(opts, { path: required, project_id: required, data: required, // what to write cb: required })); this.dbg(`write_file '${path}'`); id = uuid.v4(); data_uuid = uuid.v4(); return this.local_hub_socket((err, socket) => { var mesg; if (err) { opts.cb(err); return; } mesg = message.write_file_to_project({ id: id, project_id: project_id, path: path, data_uuid: data_uuid }); socket.write_mesg('json', mesg); socket.write_mesg('blob', { uuid: data_uuid, blob: data }); return socket.recv_mesg({ type: 'json', id: id, timeout: 10, cb: (mesg) => { switch (mesg.event) { case 'file_written_to_project': return opts.cb(); case 'error': return opts.cb(mesg.error); default: return opts.cb(`unexpected message type '${mesg.event}'`); } } }); }); } }; }).call(this); //# sourceMappingURL=local_hub_connection.js.map