Path: blob/master/src/packages/project/sage_session.ts
1447 views
//########################################################################1// This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2// License: MS-RSL – see LICENSE.md for details3//########################################################################45/*6Start the Sage server and also get a new socket connection to it.7*/89import { reuseInFlight } from "@cocalc/util/reuse-in-flight";10import { getLogger } from "@cocalc/backend/logger";11import processKill from "@cocalc/backend/misc/process-kill";12import { abspath } from "@cocalc/backend/misc_node";13import type {14Type as TCPMesgType,15Message as TCPMessage,16} from "@cocalc/backend/tcp/enable-messaging-protocol";17import { CoCalcSocket } from "@cocalc/backend/tcp/enable-messaging-protocol";18import * as message from "@cocalc/util/message";19import {20path_split,21to_json,22trunc,23trunc_middle,24uuid,25} from "@cocalc/util/misc";26import { CB } from "@cocalc/util/types/callback";27import { ISageSession, SageCallOpts } from "@cocalc/util/types/sage";28import { Client } from "./client";29import { getSageSocket } from "./sage_socket";3031const logger = getLogger("sage-session");3233//##############################################34// Direct Sage socket session -- used internally in local hub, e.g., to assist CodeMirror editors...35//##############################################3637// we have to make sure to only export the type to avoid error TS409438export type SageSessionType = InstanceType<typeof SageSession>;3940interface SageSessionOpts {41client: Client;42path: string; // the path to the *worksheet* file43}4445const cache: { [path: string]: SageSessionType } = {};4647export function sage_session(opts: Readonly<SageSessionOpts>): SageSessionType {48const { path } = opts;49// compute and cache if not cached; otherwise, get from cache:50return (cache[path] = cache[path] ?? new SageSession(opts));51}5253/*54Sage Session object5556Until you actually try to call it no socket need57*/58class SageSession implements ISageSession {59private _path: string;60private _client: Client;61private _output_cb: {62[key: string]: CB<{ done: boolean; error: string }, any>;63} = {};64private _socket: CoCalcSocket | undefined;6566constructor(opts: Readonly<SageSessionOpts>) {67this.dbg = this.dbg.bind(this);68this.dbg("constructor")();69this._path = opts.path;70this._client = opts.client;71this._output_cb = {};72}7374private dbg = (f: string) => {75return (m?: string) =>76logger.debug(`SageSession(path='${this._path}').${f}: ${m}`);77};7879close = (): void => {80if (this._socket != null) {81const pid = this._socket.pid;82if (pid != null) {83processKill(pid, 9);84}85this._socket.end();86delete this._socket;87}88for (let id in this._output_cb) {89const cb = this._output_cb[id];90cb({ done: true, error: "killed" });91}92this._output_cb = {};93delete cache[this._path];94};9596// return true if there is a socket connection to a sage server process97is_running = (): boolean => {98return this._socket != null;99};100101// NOTE: There can be many simultaneous init_socket calls at the same time,102// if e.g., the socket doesn't exist and there are a bunch of calls to @call103// at the same time.104// See https://github.com/sagemathinc/cocalc/issues/3506105// wrapped in reuseInFlight !106init_socket = reuseInFlight(async (): Promise<void> => {107const dbg = this.dbg("init_socket()");108dbg();109try {110const socket: CoCalcSocket = await getSageSocket();111112dbg("successfully opened a sage session");113this._socket = socket;114115socket.on("end", () => {116delete this._socket;117return dbg("codemirror session terminated");118});119120// CRITICAL: we must define this handler before @_init_path below,121// or @_init_path can't possibly work... since it would wait for122// this handler to get the response message!123socket.on("mesg", (type: TCPMesgType, mesg: TCPMessage) => {124dbg(`sage session: received message ${type}`);125switch (type) {126case "json":127this._handle_mesg_json(mesg);128break;129case "blob":130this._handle_mesg_blob(mesg);131break;132}133});134135await this._init_path();136} catch (err) {137if (err) {138dbg(`fail -- ${err}.`);139throw err;140}141}142});143144private _init_path = async (): Promise<void> => {145const dbg = this.dbg("_init_path()");146dbg();147return new Promise<void>((resolve, reject) => {148this.call({149input: {150event: "execute_code",151code: "os.chdir(salvus.data['path']);__file__=salvus.data['file']",152data: {153path: abspath(path_split(this._path).head),154file: abspath(this._path),155},156preparse: false,157},158cb: (resp) => {159let err: string | undefined = undefined;160if (resp.stderr) {161err = resp.stderr;162dbg(`error '${err}'`);163}164if (resp.done) {165if (err) {166reject(err);167} else {168resolve();169}170}171},172});173});174};175176public call = async ({177input,178cb,179}: Readonly<SageCallOpts>): Promise<void> => {180const dbg = this.dbg("call");181dbg(`input='${trunc(to_json(input), 300)}'`);182switch (input.event) {183case "ping":184cb({ pong: true });185return;186187case "status":188cb({ running: this.is_running() });189return;190191case "signal":192if (this._socket != null) {193dbg(`sending signal ${input.signal} to process ${this._socket.pid}`);194const pid = this._socket.pid;195if (pid != null) processKill(pid, input.signal);196}197cb({});198return;199200case "restart":201dbg("restarting sage session");202if (this._socket != null) {203this.close();204}205try {206await this.init_socket();207cb({});208} catch (err) {209cb({ error: err });210}211return;212213case "raw_input":214dbg("sending sage_raw_input event");215this._socket?.write_mesg("json", {216event: "sage_raw_input",217value: input.value,218});219return;220221default:222// send message over socket and get responses223try {224if (this._socket == null) {225await this.init_socket();226}227228if (input.id == null) {229input.id = uuid();230dbg(`generated new random uuid for input: '${input.id}' `);231}232233if (this._socket == null) {234throw new Error("no socket");235}236237this._socket.write_mesg("json", input);238239this._output_cb[input.id] = cb; // this is when opts.cb will get called...240} catch (err) {241cb({ done: true, error: err });242}243}244};245private _handle_mesg_blob = (mesg: TCPMessage) => {246const { uuid } = mesg;247let { blob } = mesg;248const dbg = this.dbg(`_handle_mesg_blob(uuid='${uuid}')`);249dbg();250251if (blob == null) {252dbg("no blob -- dropping message");253return;254}255256// This should never happen, typing enforces this to be a Buffer257if (typeof blob === "string") {258dbg("blob is string -- converting to buffer");259blob = Buffer.from(blob, "utf8");260}261262this._client.save_blob({263blob,264uuid,265cb: (err, resp) => {266if (err) {267resp = message.save_blob({268error: err,269sha1: uuid, // dumb - that sha1 should be called uuid...270});271}272this._socket?.write_mesg("json", resp);273},274});275};276277private _handle_mesg_json = (mesg: TCPMessage) => {278const dbg = this.dbg("_handle_mesg_json");279dbg(`mesg='${trunc_middle(to_json(mesg), 400)}'`);280if (mesg == null) return; // should not happen281const { id } = mesg;282if (id == null) return; // should not happen283const cb = this._output_cb[id];284if (cb != null) {285// Must do this check first since it uses done:false.286if (mesg.done || mesg.done == null) {287delete this._output_cb[id];288mesg.done = true;289}290if (mesg.done != null && !mesg.done) {291// waste of space to include done part of mesg if just false for everything else...292delete mesg.done;293}294cb(mesg);295}296};297}298299300