UNPKG

@cocalc/project

Version:
325 lines 12.5 kB
"use strict"; /* * This file is part of CoCalc: Copyright © 2020 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.ProjectInfoServer = void 0; /* Project information server, doing the heavy lifting of telling the client about what's going on in a project. */ const debug_1 = __importDefault(require("debug")); const L = (0, debug_1.default)("project:project-info:server"); const awaiting_1 = require("awaiting"); const path_1 = require("path"); const utils_1 = require("./utils"); const init_program_1 = require("../init-program"); const fs_1 = require("fs"); const server_1 = require("../terminal/server"); const server_2 = require("../x11/server"); const jupyter_1 = require("../jupyter/jupyter"); const { readFile, readdir, readlink } = fs_1.promises; const diskusage_1 = require("diskusage"); const events_1 = require("events"); //const { get_sage_path } = require("../sage_session"); function is_in_dev_project() { return process.env.SMC_LOCAL_HUB_HOME != null; } // this is a hard limit on the number of processes we gather, just to // be on the safe side to avoid processing too much data. const LIMIT = 100; const bytes2MiB = (bytes) => bytes / (1024 * 1024); class ProjectInfoServer extends events_1.EventEmitter { constructor(testing = false) { super(); this.last = undefined; this.running = false; this.delay_s = 2; this.testing = testing; this.dbg = L; } latest() { return this.last; } // this is how long the underlying machine is running // we need this information, because the processes' start time is // measured in "ticks" since the machine started async uptime() { // return uptime in secs const out = await readFile("/proc/uptime", "utf8"); const uptime = parseFloat(out.split(" ")[0]); const boottime = new Date(new Date().getTime() - 1000 * uptime); return [uptime, boottime]; } // the "stat" file contains all the information // this page explains what is what // https://man7.org/linux/man-pages/man5/proc.5.html async stat(path) { // all time-values are in seconds const raw = await readFile(path, "utf8"); // the "comm" field could contain additional spaces or parents const [i, j] = [raw.indexOf("("), raw.lastIndexOf(")")]; const start = raw.slice(0, i - 1).trim(); const end = raw.slice(j + 1).trim(); const data = `${start} comm ${end}`.split(" "); const get = (idx) => parseInt(data[idx]); // "comm" is now a placeholder to keep indices as they are. // don't forget to account for 0 vs. 1 based indexing. const ret = { ppid: get(3), state: data[2], utime: get(13) / this.ticks, stime: get(14) / this.ticks, cutime: get(15) / this.ticks, cstime: get(16) / this.ticks, starttime: get(21) / this.ticks, nice: get(18), num_threads: get(19), mem: { rss: (get(23) * this.pagesize) / (1024 * 1024) }, // MiB }; return ret; } // delta-time for this and the previous process information dt(timestamp) { return (timestamp - (this.last?.timestamp ?? 0)) / 1000; } // calculate cpu times cpu({ pid, stat, timestamp }) { // we are interested in that processes total usage: user + system const total_cpu = stat.utime + stat.stime; // the fallback is chosen in such a way, that it says 0% if we do not have historic data const prev_cpu = this.last?.processes?.[pid]?.cpu.secs ?? total_cpu; const dt = this.dt(timestamp); // how much cpu time was used since last time we checked this process… const pct = 100 * ((total_cpu - prev_cpu) / dt); return { pct: pct, secs: total_cpu }; } async cmdline(path) { // we split at the null-delimiter and filter all empty elements return (await readFile(path, "utf8")) .split("\0") .filter((c) => c.length > 0); } // for a process we know (pid, etc.) we try to map to cocalc specific information cocalc({ pid, cmdline }) { //this.dbg("classify", { pid, exe, cmdline }); if (pid === process.pid) { return { type: "project" }; } // TODO use get_sage_path to get a path to a sagews const jupyter_kernel = (0, jupyter_1.get_kernel_by_pid)(pid); if (jupyter_kernel != null) { return { type: "jupyter", path: jupyter_kernel.get_path() }; } const termpath = (0, server_1.pid2path)(pid); if (termpath != null) { return { type: "terminal", path: termpath }; } const x11_path = (0, server_2.get_path_for_pid)(pid); if (x11_path != null) { return { type: "x11", path: x11_path }; } // SSHD: strangely, just one long string in cmdline[0] if (cmdline.length === 1 && cmdline[0].startsWith("sshd:") && cmdline[0].indexOf("-p 2222") != -1) { return { type: "sshd" }; } } // this gathers all the information for a specific process with the given pid async process({ pid: pid_str, uptime, timestamp }) { const base = (0, path_1.join)("/proc", pid_str); const pid = parseInt(pid_str); const fn = (name) => (0, path_1.join)(base, name); const [cmdline, exe, stat] = await Promise.all([ this.cmdline(fn("cmdline")), readlink(fn("exe")), this.stat(fn("stat")), ]); const data = { pid, ppid: stat.ppid, cmdline, exe, stat, cpu: this.cpu({ pid, timestamp, stat }), uptime: uptime - stat.starttime, cocalc: this.cocalc({ pid, cmdline }), }; return data; } // this is where we gather information about all running processes async processes({ timestamp, uptime }) { const procs = {}; let n = 0; for (const pid of await readdir("/proc")) { if (!pid.match(/^[0-9]+$/)) continue; try { const proc = await this.process({ pid, uptime, timestamp }); procs[proc.pid] = proc; } catch (err) { if (this.testing) this.dbg(`process ${pid} likely vanished – could happen – ${err}`); } // we avoid processing and sending too much data if (n > LIMIT) { this.dbg(`too many processes – limit of ${LIMIT} reached!`); break; } else { n += 1; } } return procs; } // this is specific to running a project in a CGroup container // however, even without a container this shouldn't fail … just tells // you what the whole system is doing, all your processes,… // NOTE: most of this replaces kucalc.coffee async cgroup({ timestamp }) { if (!is_in_dev_project() && !init_program_1.options.kucalc && !this.testing) return; const [mem_stat_raw, cpu_raw, oom_raw, cfs_quota_raw, cfs_period_raw,] = await Promise.all([ readFile("/sys/fs/cgroup/memory/memory.stat", "utf8"), readFile("/sys/fs/cgroup/cpu,cpuacct/cpuacct.usage", "utf8"), readFile("/sys/fs/cgroup/memory/memory.oom_control", "utf8"), readFile("/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us", "utf8"), readFile("/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us", "utf8"), ]); const mem_stat_keys = [ "total_rss", "total_cache", "hierarchical_memory_limit", ]; const cpu_usage = parseFloat(cpu_raw) / Math.pow(10, 9); const dt = this.dt(timestamp); const cpu_usage_rate = this.last?.cgroup != null ? (cpu_usage - this.last.cgroup.cpu_usage) / dt : 0; const [cfs_quota, cfs_period] = [ parseInt(cfs_quota_raw), parseInt(cfs_period_raw), ]; const mem_stat = mem_stat_raw .split("\n") .map((line) => line.split(" ")) .filter(([k, _]) => mem_stat_keys.includes(k)) .reduce((stat, [key, val]) => { stat[key] = bytes2MiB(parseInt(val)); return stat; }, {}); const oom_kills = oom_raw .split("\n") .filter((val) => val.startsWith("oom_kill ")) .map((val) => parseInt(val.slice("oom_kill ".length)))[0]; return { mem_stat, cpu_usage, cpu_usage_rate, cpu_cores_limit: cfs_quota / cfs_period, oom_kills, }; } // for cocalc/kucalc we want to know the disk usage + limits of the // users home dir and /tmp. /tmp is a ram disk, which will count against // the overall memory limit! async disk_usage() { const convert = function (val) { return { total: bytes2MiB(val.total), free: bytes2MiB(val.free), available: bytes2MiB(val.available), usage: bytes2MiB(val.total - val.free), }; }; const [tmp, project] = await Promise.all([ (0, diskusage_1.check)("/tmp"), (0, diskusage_1.check)(process.env.HOME ?? "/home/user"), ]); return { tmp: convert(tmp), project: convert(project) }; } // this grabs some kernel configuration values we need. they won't change async init() { if (this.ticks == null) { const [p_ticks, p_pagesize] = await Promise.all([ (0, utils_1.exec)("getconf CLK_TCK"), (0, utils_1.exec)("getconf PAGESIZE"), ]); // should be 100, usually this.ticks = parseInt(p_ticks.stdout.trim()); // 4096? this.pagesize = parseInt(p_pagesize.stdout.trim()); } } // orchestrating where all the information is bundled up for an update async get_info() { const [uptime, boottime] = await this.uptime(); const timestamp = new Date().getTime(); const [processes, cgroup, disk_usage] = await Promise.all([ this.processes({ uptime, timestamp }), this.cgroup({ timestamp }), this.disk_usage(), ]); const info = { timestamp, processes, uptime, boottime, cgroup, disk_usage, }; return info; } stop() { this.running = false; } async start() { if (this.running) { this.dbg("project-info/server: already running, cannot be started twice"); } else { await this._start(); } } async _start() { this.dbg("start"); if (this.running) { throw Error("Cannot start ProjectInfoServer twice"); } this.running = true; await this.init(); while (true) { //this.dbg(`listeners on 'info': ${this.listenerCount("info")}`); const info = await this.get_info(); this.last = info; this.emit("info", info); if (this.running) { await (0, awaiting_1.delay)(1000 * this.delay_s); } else { this.dbg("start: no longer running → stopping loop"); this.last = undefined; return; } // in test mode just one more, that's enough if (this.last != null && this.testing) { const info = await this.get_info(); this.dbg(JSON.stringify(info, null, 2)); return; } } } } exports.ProjectInfoServer = ProjectInfoServer; // testing: $ ts-node server.ts if (require.main === module) { const pis = new ProjectInfoServer(true); pis.start().then(() => process.exit()); } //# sourceMappingURL=server.js.map