UNPKG

@cocalc/server

Version:

CoCalc server functionality: functions used by either the hub and the next.js server

173 lines (169 loc) 6.74 kB
"use strict"; /* * This file is part of CoCalc: Copyright © 2022 Sagemath, Inc. * License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details */ 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; /* 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. */ const async_utils_1 = require("@cocalc/util/async-utils"); const database_1 = require("@cocalc/database"); const events_1 = require("events"); const lodash_1 = require("lodash"); const quota_1 = require("@cocalc/util/upgrades/quota"); const awaiting_1 = require("awaiting"); const logger_1 = __importDefault(require("@cocalc/backend/logger")); const hook_1 = require("@cocalc/database/postgres/site-license/hook"); const winston = (0, 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. const projectCache = {}; function getProject(project_id) { return projectCache[project_id]?.deref(); } exports.getProject = getProject; class BaseProject extends events_1.EventEmitter { constructor(project_id) { super(); this.is_ready = false; this.is_freed = false; this.stateChanging = undefined; projectCache[project_id] = new WeakRef(this); this.project_id = project_id; const dbg = this.dbg("constructor"); dbg("initializing"); } async siteLicenseHook() { await (0, hook_1.site_license_hook)((0, database_1.db)(), this.project_id); } async saveStateToDatabase(state) { await (0, async_utils_1.callback2)((0, database_1.db)().set_project_state, { ...state, project_id: this.project_id, }); } async saveStatusToDatabase(status) { await (0, async_utils_1.callback2)((0, database_1.db)().set_project_status, { project_id: this.project_id, status, }); } dbg(f) { return (msg) => { winston.debug(`(project_id=${this.project_id}).${f}: ${msg}`); }; } async restart() { this.dbg("restart")(); await this.stop(); await this.start(); } async wait(opts) { const { until, maxTime } = opts; const t0 = new Date().valueOf(); let d = 250; while (new Date().valueOf() - t0 <= maxTime) { if (await until()) { winston.debug(`wait ${this.project_id} -- satisfied`); return; } await (0, awaiting_1.delay)(d); d *= 1.2; } const 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. async address() { const dbg = this.dbg("address"); dbg("first ensure is running"); await this.start(); dbg("it is running"); const status = await this.status(); if (!status["hub-server.port"]) { throw Error("unable to determine project port"); } if (!status["secret_token"]) { throw Error("unable to determine secret_token"); } const state = await this.state(); const host = state.ip; if (!host) { throw Error("unable to determine host"); } return { 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. */ async setAllQuotas() { const dbg = this.dbg("set_all_quotas"); dbg(); // 1. Get data about project from the database, namely: // - is project currently running (if not, nothing to do) // - if running, what quotas it was started with and what its quotas are now // 2. If quotas differ *AND* project is running, restarts project. const x = await (0, async_utils_1.callback2)((0, database_1.db)().get_project, { project_id: this.project_id, columns: ["state", "users", "settings", "run_quota"], }); if (!["running", "starting", "pending"].includes(x.state?.state)) { dbg("project not active so nothing to do"); return; } // FIX: this quota call misses site_licenses and server_settings // https://github.com/sagemathinc/cocalc/issues/5633 const cur = (0, quota_1.quota)(x.settings, x.users); if ((0, lodash_1.isEqual)(x.run_quota, cur)) { dbg("running, but no quotas changed"); 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. (async () => { try { await this.restart(); dbg("restart worked"); } catch (err) { dbg(`restart failed -- ${err}`); } })(); } } } exports.BaseProject = BaseProject; //# sourceMappingURL=base.js.map