@cocalc/project
Version:
CoCalc: project daemon
120 lines (110 loc) • 3.66 kB
text/typescript
/*
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
* License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details
*/
import basePath from "@cocalc/backend/base-path";
import { data } from "@cocalc/backend/data";
import { project_id } from "@cocalc/project/data";
import { INFO } from "@cocalc/project/info-json";
import { getLogger } from "@cocalc/project/logger";
import { NamedServerName } from "@cocalc/util/types/servers";
import { exec } from "child_process";
import {
mkdir as mkdir0,
readFile as readFile0,
writeFile as writeFile0,
} from "fs";
import getPort from "get-port";
import { join } from "path";
import { promisify } from "util";
import getSpec from "./list";
const mkdir = promisify(mkdir0);
const readFile = promisify(readFile0);
const writeFile = promisify(writeFile0);
const winston = getLogger("named-servers:control");
// Returns the port or throws an exception.
export async function start(name: NamedServerName): Promise<number> {
winston.debug(`start ${name}`);
const s = await status(name);
if (s.status == "running") {
winston.debug(`${name} is already running`);
return s.port;
}
const port = await getPort({ port: preferredPort(name) });
// For servers that need a basePath, they will use this one.
// Other servers (e.g., Pluto, code-server) that don't need
// a basePath because they use only relative URL's are accessed
// via .../project_id/server/${port}.
let ip = INFO.location.host ?? "127.0.0.1";
if (ip == "localhost") {
ip = "127.0.0.1";
}
const base = join(basePath, `/${project_id}/port/${name}`);
const cmd = await getCommand(name, ip, port, base);
winston.debug(`will start ${name} by running "${cmd}"`);
const child = exec(cmd, { cwd: process.env.HOME });
const p = await paths(name);
await writeFile(p.port, `${port}`);
await writeFile(p.pid, `${child.pid}`);
await writeFile(p.command, `$!/bin/sh\n${cmd}\n`);
return port;
}
async function getCommand(
name: NamedServerName,
ip: string,
port: number,
base: string
): Promise<string> {
const { stdout, stderr } = await paths(name);
const spec = getSpec(name);
const cmd = spec(ip, port, base);
return `${cmd} 1>${stdout} 2>${stderr}`;
}
// Returns the status and port (if defined).
export async function status(
name: string
): Promise<{ status: "running"; port: number } | { status: "stopped" }> {
const { pid, port } = await paths(name);
try {
const pidValue = parseInt((await readFile(pid)).toString());
// it might be running
process.kill(pidValue, 0); // throws error if NOT running
// it is running
const portValue = parseInt((await readFile(port)).toString());
// and there is a port file,
// and the port is a number.
if (!Number.isInteger(portValue)) {
throw Error("invalid port");
}
return { status: "running", port: portValue };
} catch (_err) {
// it's not running or the port isn't valid
return { status: "stopped" };
}
}
async function paths(name: string): Promise<{
pid: string;
stderr: string;
stdout: string;
port: string;
command: string;
}> {
const path = join(data, "named_servers", name);
try {
await mkdir(path, { recursive: true });
} catch (_err) {
// probably already exists
}
return {
pid: join(path, "pid"),
stderr: join(path, "stderr"),
stdout: join(path, "stdout"),
port: join(path, "port"),
command: join(path, "command.sh"),
};
}
function preferredPort(name: string): number | undefined {
const p = process.env[`COCALC_${name.toUpperCase()}_PORT`];
if (p == null) return p;
return parseInt(p);
}