Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/project/project-info/server.ts
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45/*6Project information server, doing the heavy lifting of telling the client7about what's going on in a project.8*/910import { delay } from "awaiting";11import type { DiskUsage as DF_DiskUsage } from "diskusage";12import { check as df } from "diskusage";13import { EventEmitter } from "node:events";14import { readFile } from "node:fs/promises";1516import { ProcessStats } from "@cocalc/backend/process-stats";17import { get_kernel_by_pid } from "@cocalc/jupyter/kernel";18import { pidToPath as terminalPidToPath } from "@cocalc/terminal";19import {20CGroup,21CoCalcInfo,22DiskUsage,23Process,24Processes,25ProjectInfo,26} from "@cocalc/util/types/project-info/types";27import { get_path_for_pid as x11_pid2path } from "../x11/server";28//import { get_sage_path } from "../sage_session"29import { getLogger } from "../logger";3031const L = getLogger("project-info:server").debug;3233// function is_in_dev_project() {34// return process.env.SMC_LOCAL_HUB_HOME != null;35// }3637const bytes2MiB = (bytes) => bytes / (1024 * 1024);3839export class ProjectInfoServer extends EventEmitter {40private last?: ProjectInfo = undefined;41private readonly dbg: Function;42private running = false;43private readonly testing: boolean;44private delay_s: number;45private cgroupFilesAreMissing: boolean = false;46private processStats: ProcessStats;4748constructor(testing = false) {49super();50this.delay_s = 2;51this.testing = testing;52this.dbg = L;53}5455private async processes(timestamp: number) {56return await this.processStats.processes(timestamp);57}5859// delta-time for this and the previous process information60private dt(timestamp) {61return (timestamp - (this.last?.timestamp ?? 0)) / 1000;62}6364public latest(): ProjectInfo | undefined {65return this.last;66}6768// for a process we know (pid, etc.) we try to map to cocalc specific information69private cocalc({70pid,71cmdline,72}: Pick<Process, "pid" | "cmdline">): CoCalcInfo | undefined {73//this.dbg("classify", { pid, exe, cmdline });74if (pid === process.pid) {75return { type: "project" };76}77// TODO use get_sage_path to get a path to a sagews78const jupyter_kernel = get_kernel_by_pid(pid);79if (jupyter_kernel != null) {80return { type: "jupyter", path: jupyter_kernel.get_path() };81}82const termpath = terminalPidToPath(pid);83if (termpath != null) {84return { type: "terminal", path: termpath };85}86const x11_path = x11_pid2path(pid);87if (x11_path != null) {88return { type: "x11", path: x11_path };89}90// SSHD: strangely, just one long string in cmdline[0]91if (92cmdline.length === 1 &&93cmdline[0].startsWith("sshd:") &&94cmdline[0].indexOf("-p 2222") != -195) {96return { type: "sshd" };97}98}99100private lookupCoCalcInfo(processes: Processes) {101// iterate over all processes keys (pid) and call this.cocalc({pid, cmdline})102// to update the processes coclc field103for (const pid in processes) {104processes[pid].cocalc = this.cocalc({105pid: parseInt(pid),106cmdline: processes[pid].cmdline,107});108}109}110111// this is specific to running a project in a CGroup container112// Harald: however, even without a container this shouldn't fail … just tells113// you what the whole system is doing, all your processes.114// William: it's constantly failing in cocalc-docker every second, so to avoid115// clogging logs and wasting CPU, if the files are missing once, it stops updating.116private async cgroup({ timestamp }): Promise<CGroup | undefined> {117if (this.cgroupFilesAreMissing) {118return;119}120try {121const [mem_stat_raw, cpu_raw, oom_raw, cfs_quota_raw, cfs_period_raw] =122await Promise.all([123readFile("/sys/fs/cgroup/memory/memory.stat", "utf8"),124readFile("/sys/fs/cgroup/cpu,cpuacct/cpuacct.usage", "utf8"),125readFile("/sys/fs/cgroup/memory/memory.oom_control", "utf8"),126readFile("/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us", "utf8"),127readFile("/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us", "utf8"),128]);129const mem_stat_keys = [130"total_rss",131"total_cache",132"hierarchical_memory_limit",133];134const cpu_usage = parseFloat(cpu_raw) / Math.pow(10, 9);135const dt = this.dt(timestamp);136const cpu_usage_rate =137this.last?.cgroup != null138? (cpu_usage - this.last.cgroup.cpu_usage) / dt139: 0;140const [cfs_quota, cfs_period] = [141parseInt(cfs_quota_raw),142parseInt(cfs_period_raw),143];144const mem_stat = mem_stat_raw145.split("\n")146.map((line) => line.split(" "))147.filter(([k, _]) => mem_stat_keys.includes(k))148.reduce((stat, [key, val]) => {149stat[key] = bytes2MiB(parseInt(val));150return stat;151}, {});152const oom_kills = oom_raw153.split("\n")154.filter((val) => val.startsWith("oom_kill "))155.map((val) => parseInt(val.slice("oom_kill ".length)))[0];156return {157mem_stat,158cpu_usage,159cpu_usage_rate,160cpu_cores_limit: cfs_quota / cfs_period,161oom_kills,162};163} catch (err) {164this.dbg("cgroup: error", err);165if (err.code == "ENOENT") {166// TODO: instead of shutting this down, we could maybe do a better job167// figuring out what the correct cgroups files are on a given system.168// E.g., in my cocalc-docker, I do NOT have /sys/fs/cgroup/memory/memory.stat169// but I do have /sys/fs/cgroup/memory.stat170this.cgroupFilesAreMissing = true;171this.dbg(172"cgroup: files are missing so cgroups info will no longer be updated",173);174}175return undefined;176}177}178179// for cocalc/kucalc we want to know the disk usage + limits of the180// users home dir and /tmp. /tmp is a ram disk, which will count against181// the overall memory limit!182private async disk_usage(): Promise<DiskUsage> {183const convert = function (val: DF_DiskUsage) {184return {185total: bytes2MiB(val.total),186free: bytes2MiB(val.free),187available: bytes2MiB(val.available),188usage: bytes2MiB(val.total - val.free),189};190};191const [tmp, project] = await Promise.all([192df("/tmp"),193df(process.env.HOME ?? "/home/user"),194]);195return { tmp: convert(tmp), project: convert(project) };196}197198// orchestrating where all the information is bundled up for an update199private async get_info(): Promise<ProjectInfo | undefined> {200try {201const timestamp = Date.now();202const [processes, cgroup, disk_usage] = await Promise.all([203this.processes(timestamp),204this.cgroup({ timestamp }),205this.disk_usage(),206]);207const { procs, boottime, uptime } = processes;208this.lookupCoCalcInfo(procs);209const info: ProjectInfo = {210timestamp,211processes: procs,212uptime,213boottime,214cgroup,215disk_usage,216};217return info;218} catch (err) {219this.dbg("get_info: error", err);220}221}222223public stop() {224this.running = false;225}226227public async start(): Promise<void> {228if (this.running) {229this.dbg("project-info/server: already running, cannot be started twice");230} else {231await this._start();232}233}234235private async _start(): Promise<void> {236this.dbg("start");237if (this.running) {238throw Error("Cannot start ProjectInfoServer twice");239}240this.running = true;241this.processStats = new ProcessStats({242testing: this.testing,243dbg: this.dbg,244});245await this.processStats.init();246while (true) {247//this.dbg(`listeners on 'info': ${this.listenerCount("info")}`);248const info = await this.get_info();249if (info != null) this.last = info;250this.emit("info", info ?? this.last);251if (this.running) {252await delay(1000 * this.delay_s);253} else {254this.dbg("start: no longer running → stopping loop");255this.last = undefined;256return;257}258// in test mode just one more, that's enough259if (this.last != null && this.testing) {260const info = await this.get_info();261this.dbg(JSON.stringify(info, null, 2));262return;263}264}265}266}267268// testing: $ ts-node server.ts269if (require.main === module) {270const pis = new ProjectInfoServer(true);271pis.start().then(() => process.exit());272}273274275