UNPKG

smc-hub

Version:

CoCalc: Backend webserver component

318 lines (314 loc) 15.1 kB
"use strict"; /* Project control abstract base class. The hub uses this to get information about a project and do some basic tasks. There are different implementations for different ways in which cocalc gets deployed. This module does 3 things: 1. CONTROL: Start/stop/restart a project. 2. CONNECT: Get ports, ip address, and the project secret token 3. COPY: Copying a directory of files from one project to another. For simplicity, it doesn't do anything else. It's good to keep this as small as possible, so it is manageable, especially as we adapt CoCalc to new environments. */ var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.BaseProject = exports.getProject = void 0; var async_utils_1 = require("smc-util/async-utils"); var database_1 = require("smc-hub/servers/database"); var events_1 = require("events"); var lodash_1 = require("lodash"); var quota_1 = require("smc-util/upgrades/quota"); var awaiting_1 = require("awaiting"); var logger_1 = __importDefault(require("smc-hub/logger")); var hook_1 = require("smc-hub/postgres/site-license/hook"); var winston = logger_1.default("project-control"); // We use a cache to ensure that there is at most one copy of a given Project // for each project_id, since internally we assume this in some cases, e.g., // when starting a project we rely on the internal stateChanging attribute // rather than the database to know that we're starting the project. We use // WeakRef so that when nothing is referencing the project, it can be garbage // collected. These objects don't use much memory, but blocking garbage collection // would be bad. var projectCache = {}; function getProject(project_id) { var _a; return (_a = projectCache[project_id]) === null || _a === void 0 ? void 0 : _a.deref(); } exports.getProject = getProject; var BaseProject = /** @class */ (function (_super) { __extends(BaseProject, _super); function BaseProject(project_id) { var _this = _super.call(this) || this; _this.is_ready = false; _this.is_freed = false; _this.stateChanging = undefined; projectCache[project_id] = new WeakRef(_this); _this.project_id = project_id; var dbg = _this.dbg("constructor"); dbg("initializing"); return _this; } BaseProject.prototype.siteLicenseHook = function () { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, hook_1.site_license_hook(database_1.database, this.project_id)]; case 1: _a.sent(); return [2 /*return*/]; } }); }); }; BaseProject.prototype.saveStateToDatabase = function (state) { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, async_utils_1.callback2(database_1.database.set_project_state, __assign(__assign({}, state), { project_id: this.project_id }))]; case 1: _a.sent(); return [2 /*return*/]; } }); }); }; BaseProject.prototype.saveStatusToDatabase = function (status) { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, async_utils_1.callback2(database_1.database.set_project_status, { project_id: this.project_id, status: status, })]; case 1: _a.sent(); return [2 /*return*/]; } }); }); }; BaseProject.prototype.dbg = function (f) { var _this = this; return function (msg) { winston.debug("(project_id=" + _this.project_id + ")." + f + ": " + msg); }; }; BaseProject.prototype.restart = function () { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: this.dbg("restart")(); return [4 /*yield*/, this.stop()]; case 1: _a.sent(); return [4 /*yield*/, this.start()]; case 2: _a.sent(); return [2 /*return*/]; } }); }); }; BaseProject.prototype.wait = function (opts) { return __awaiter(this, void 0, void 0, function () { var until, maxTime, t0, d, err; return __generator(this, function (_a) { switch (_a.label) { case 0: until = opts.until, maxTime = opts.maxTime; t0 = new Date().valueOf(); d = 250; _a.label = 1; case 1: if (!(new Date().valueOf() - t0 <= maxTime)) return [3 /*break*/, 4]; return [4 /*yield*/, until()]; case 2: if (_a.sent()) { winston.debug("wait " + this.project_id + " -- satisfied"); return [2 /*return*/]; } return [4 /*yield*/, awaiting_1.delay(d)]; case 3: _a.sent(); d *= 1.2; return [3 /*break*/, 1]; case 4: err = "wait " + this.project_id + " -- FAILED"; winston.debug(err); throw Error(err); } }); }); }; // Everything the hub needs to know to connect to the project // via the TCP connection. Raises error if anything can't be // determined. BaseProject.prototype.address = function () { return __awaiter(this, void 0, void 0, function () { var dbg, status, state, host; return __generator(this, function (_a) { switch (_a.label) { case 0: dbg = this.dbg("address"); dbg("first ensure is running"); return [4 /*yield*/, this.start()]; case 1: _a.sent(); dbg("it is running"); return [4 /*yield*/, this.status()]; case 2: status = _a.sent(); if (!status["hub-server.port"]) { throw Error("unable to determine hub-server port"); } if (!status["secret_token"]) { throw Error("unable to determine secret_token"); } return [4 /*yield*/, this.state()]; case 3: state = _a.sent(); host = state.ip; if (!host) { throw Error("unable to determine host"); } return [2 /*return*/, { host: host, port: status["hub-server.port"], secret_token: status.secret_token, }]; } }); }); }; /* set_all_quotas ensures that if the project is running and the quotas (except idle_timeout) have changed, then the project is restarted. */ BaseProject.prototype.setAllQuotas = function () { var _a; return __awaiter(this, void 0, void 0, function () { var dbg, x, cur; var _this = this; return __generator(this, function (_b) { switch (_b.label) { case 0: dbg = this.dbg("set_all_quotas"); dbg(); return [4 /*yield*/, async_utils_1.callback2(database_1.database.get_project, { project_id: this.project_id, columns: ["state", "users", "settings", "run_quota"], })]; case 1: x = _b.sent(); if (!["running", "starting", "pending"].includes((_a = x.state) === null || _a === void 0 ? void 0 : _a.state)) { dbg("project not active so nothing to do"); return [2 /*return*/]; } cur = quota_1.quota(x.settings, x.users); if (lodash_1.isEqual(x.run_quota, cur)) { dbg("running, but no quotas changed"); return [2 /*return*/]; } else { dbg("running and a quota changed; restart"); // CRITICAL: do NOT await on this restart! The set_all_quotas call must // complete quickly (in an HTTP requrest), whereas restart can easily take 20s, // and there is no reason to wait on this. Wrapping this as below calls the // function, properly awaits and logs what happens, and avoids uncaught exceptions, // but doesn't block the caller of this function. (function () { return __awaiter(_this, void 0, void 0, function () { var err_1; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 2, , 3]); return [4 /*yield*/, this.restart()]; case 1: _a.sent(); dbg("restart worked"); return [3 /*break*/, 3]; case 2: err_1 = _a.sent(); dbg("restart failed -- " + err_1); return [3 /*break*/, 3]; case 3: return [2 /*return*/]; } }); }); })(); } return [2 /*return*/]; } }); }); }; return BaseProject; }(events_1.EventEmitter)); exports.BaseProject = BaseProject; //# sourceMappingURL=base.js.map