smc-hub
Version:
CoCalc: Backend webserver component
1,278 lines (1,213 loc) • 45.4 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
//########################################################################
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