UNPKG

@cocalc/server

Version:

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

202 lines (201 loc) 7.82 kB
"use strict"; /* Compute client for use in Kubernetes cluster by the hub. This **modifies the database** to get "something out there (manage-actions) to" start and stop the project, copy files between projects, etc. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const base_1 = require("./base"); const database_1 = require("@cocalc/database"); const async_utils_1 = require("@cocalc/util/async-utils"); const misc_1 = require("@cocalc/util/misc"); const logger_1 = __importDefault(require("@cocalc/backend/logger")); const winston = (0, logger_1.default)("project-control-kucalc"); class Project extends base_1.BaseProject { constructor(project_id) { super(project_id); } async get(columns) { return await (0, async_utils_1.callback2)((0, database_1.db)().get_project, { project_id: this.project_id, columns, }); } async state() { return (await this.get(["state"]))?.state ?? {}; } async status() { const status = (await this.get(["status"]))?.status ?? {}; // In KuCalc the ports for various services are hardcoded constants, // and not actually storted in the database, so we put them here. // This is also hardcoded in kucalc's addons/project/image/init/init.sh (!) status["hub-server.port"] = 6000; status["browser-server.port"] = 6001; return status; } async start() { if (this.stateChanging != null) return; winston.info(`start ${this.project_id}`); if ((await this.state()).state == "running") { winston.debug("start -- already running"); return; } try { this.stateChanging = { state: "starting" }; await this.siteLicenseHook(); await this.actionRequest("start"); await this.waitUntilProject((project) => project.state?.state == "running" || project.action_request?.finished, 120); } finally { this.stateChanging = undefined; } } async stop() { if (this.stateChanging != null) return; winston.info("stop ", this.project_id); if ((await this.state()).state != "running") { return; } try { this.stateChanging = { state: "stopping" }; await this.actionRequest("stop"); await this.waitUntilProject((project) => (project.state != null && project.state != "running" && project.state != "stopping") || project.action_request?.finished, 60); } finally { this.stateChanging = undefined; } } async copyPath(opts) { const dbg = this.dbg("copyPath"); dbg(JSON.stringify(opts)); if (opts.path == null) { throw Error("path must be specified"); } opts.target_project_id = opts.target_project_id ? opts.target_project_id : this.project_id; opts.target_path = opts.target_path ? opts.target_path : opts.path; // check UUID are valid if (!(0, misc_1.is_valid_uuid_string)(opts.target_project_id)) { throw Error(`target_project_id=${opts.target_project_id} is invalid`); } const copyID = (0, misc_1.uuid)(); dbg(`copyID=${copyID}`); if (opts.scheduled && opts.scheduled instanceof Date) { // We have to remove the timezone info, b/c the PostgreSQL field is without timezone. // Ideally though, this is always UTC, e.g. "2019-08-08T18:34:49". const d = new Date(opts.scheduled); const offset = d.getTimezoneOffset() / 60; opts.scheduled = new Date(d.getTime() - offset); opts.wait_until_done = false; dbg(`opts.scheduled = ${opts.scheduled}`); } dbg("write query requesting the copy to happen to the database"); await (0, async_utils_1.callback2)((0, database_1.db)()._query, { query: "INSERT INTO copy_paths", values: { "id ::UUID": copyID, "time ::TIMESTAMP": new Date(), "source_project_id ::UUID": this.project_id, "source_path ::TEXT": opts.path, "target_project_id ::UUID": opts.target_project_id, "target_path ::TEXT": opts.target_path, "overwrite_newer ::BOOLEAN": opts.overwrite_newer, "public ::BOOLEAN": opts.public, "delete_missing ::BOOLEAN": opts.delete_missing, "backup ::BOOLEAN": opts.backup, "bwlimit ::TEXT": opts.bwlimit, "timeout ::NUMERIC": opts.timeout, "scheduled ::TIMESTAMP": opts.scheduled, "exclude ::TEXT[]": opts.exclude, }, }); if (opts.wait_until_done == true) { dbg("waiting for the copy request to complete..."); await this.waitUntilCopyFinished(copyID, 60 * 4); dbg("finished"); return ""; } else { dbg("NOT waiting for copy to complete"); return copyID; } } getProjectSynctable() { // this is all in coffeescript, hence the any type above. return (0, database_1.db)().synctable({ table: "projects", columns: ["state", "action_request"], where: { "project_id = $::UUID": this.project_id }, // where_function is a fast easy test for matching: where_function: (project_id) => project_id == this.project_id, }); } async actionRequest(action) { await (0, async_utils_1.callback2)((0, database_1.db)()._query, { query: "UPDATE projects", where: { "project_id = $::UUID": this.project_id }, jsonb_set: { action_request: { action, time: new Date(), started: undefined, finished: undefined, }, }, }); } async waitUntilProject(until, timeout // in seconds ) { let synctable = undefined; try { synctable = this.getProjectSynctable(); await (0, async_utils_1.callback2)(synctable.wait, { until: () => until(synctable.get(this.project_id)?.toJS() ?? {}), timeout, }); } finally { synctable?.close(); } } getCopySynctable(copyID) { return (0, database_1.db)().synctable({ table: "copy_paths", columns: ["started", "error", "finished"], where: { "id = $::UUID": copyID }, where_function: (id) => id == copyID, }); } async waitUntilCopyFinished(copyID, timeout // in seconds ) { let synctable = undefined; try { synctable = this.getCopySynctable(copyID); await (0, async_utils_1.callback2)(synctable.wait, { until: () => synctable.getIn([copyID, "finished"]), timeout, }); const err = synctable.getIn([copyID, "error"]); if (err) { throw Error(err); } } finally { synctable?.close(); } } } function get(project_id) { return (0, base_1.getProject)(project_id) ?? new Project(project_id); } exports.default = get; //# sourceMappingURL=kucalc.js.map