UNPKG

@cocalc/server

Version:

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

189 lines (161 loc) 5.23 kB
/* * This file is part of CoCalc: Copyright © 2022 Sagemath, Inc. * License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details */ /* This is meant to run on a multi-user system, but where the hub runs as a single user and all projects also run as that same user, but with there own HOME directories. There is thus no security or isolation at all between projects. There is still a notion of multiple cocalc projects and cocalc users. This is useful for: - development of cocalc from inside of a CoCalc project - non-collaborative use of cocalc on your own laptop, e.g., when you're on an airplane. */ import { kill } from "process"; import { copyPath, ensureConfFilesExists, getEnvironment, getProjectPID, getState, getStatus, homePath, isProjectRunning, launchProjectDaemon, mkdir, setupDataPath, } from "./util"; import { BaseProject, CopyOptions, ProjectStatus, ProjectState, getProject, } from "./base"; import getLogger from "@cocalc/backend/logger"; import { query } from "@cocalc/database/postgres/query"; import { db } from "@cocalc/database"; import { quota } from "@cocalc/util/upgrades/quota"; const winston = getLogger("project-control:single-user"); // Usually should fully start in about 5 seconds, but we give it 20s. const MAX_START_TIME_MS = 20000; const MAX_STOP_TIME_MS = 10000; class Project extends BaseProject { private HOME: string; constructor(project_id: string) { super(project_id); this.HOME = homePath(this.project_id); } async state(): Promise<ProjectState> { if (this.stateChanging != null) { return this.stateChanging; } const state = await getState(this.HOME); this.saveStateToDatabase(state); return state; } async status(): Promise<ProjectStatus> { const status = await getStatus(this.HOME); // TODO: don't include secret token in log message. winston.debug( `got status of ${this.project_id} = ${JSON.stringify(status)}` ); await this.saveStatusToDatabase(status); return status; } async start(): Promise<void> { winston.debug("start", this.project_id); if (this.stateChanging != null) return; // Home directory const HOME = this.HOME; if (await isProjectRunning(HOME)) { winston.debug("start -- already running"); await this.saveStateToDatabase({ state: "running" }); return; } try { this.stateChanging = { state: "starting" }; await this.saveStateToDatabase(this.stateChanging); await this.siteLicenseHook(); await this.setRunQuota(); await mkdir(HOME, { recursive: true }); await ensureConfFilesExists(HOME); // this.get('env') = extra env vars for project (from synctable): const env = await getEnvironment(this.project_id); winston.debug(`start ${this.project_id}: env = ${JSON.stringify(env)}`); // Setup files await setupDataPath(HOME); // Fork and launch project server await launchProjectDaemon(env); await this.wait({ until: async () => { if (!(await isProjectRunning(this.HOME))) { return false; } const status = await this.status(); return !!status.secret_token && !!status["hub-server.port"]; }, maxTime: MAX_START_TIME_MS, }); } finally { this.stateChanging = undefined; // ensure state valid in database await this.state(); } } async stop(): Promise<void> { if (this.stateChanging != null) return; winston.debug("stop ", this.project_id); if (!(await isProjectRunning(this.HOME))) { await this.saveStateToDatabase({ state: "opened" }); return; } try { this.stateChanging = { state: "stopping" }; await this.saveStateToDatabase(this.stateChanging); try { const pid = await getProjectPID(this.HOME); kill(-pid); } catch (_err) { // expected exception if no pid } await this.wait({ until: async () => !(await isProjectRunning(this.HOME)), maxTime: MAX_STOP_TIME_MS, }); } finally { this.stateChanging = undefined; // ensure state valid. await this.state(); } } async copyPath(opts: CopyOptions): Promise<string> { winston.debug("copyPath ", this.project_id, opts); await copyPath(opts, this.project_id); return ""; } // despite not being used, this is useful for development and // some day in the future the run_quota will be shown in the UI async setRunQuota(): Promise<void> { const { settings, users, site_license } = await query({ db: db(), select: ["site_license", "settings", "users"], table: "projects", where: { project_id: this.project_id }, one: true, }); const run_quota = quota(settings, users, site_license); await query({ db: db(), query: "UPDATE projects", where: { project_id: this.project_id }, jsonb_set: { run_quota }, }); winston.debug("updated run_quota=", run_quota); } } export default function get(project_id: string): Project { return (getProject(project_id) as Project) ?? new Project(project_id); }