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/sage_session.ts
Views: 687
//########################################################################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 { get_sage_socket } from "./sage_socket";3031// import { ExecuteCodeOutput } from "@cocalc/util/types/execute-code";3233const winston = getLogger("sage-session");3435//##############################################36// Direct Sage socket session -- used internally in local hub, e.g., to assist CodeMirror editors...37//##############################################3839// we have to make sure to only export the type to avoid error TS409440export type SageSessionType = InstanceType<typeof SageSession>;4142interface SageSessionOpts {43client: Client;44path: string; // the path to the *worksheet* file45}4647const cache: { [path: string]: SageSessionType } = {};4849export function sage_session(opts: Readonly<SageSessionOpts>): SageSessionType {50const { path } = opts;51// compute and cache if not cached; otherwise, get from cache:52return (cache[path] = cache[path] ?? new SageSession(opts));53}54// TODO for project-info/server we need a function that returns a path to a sage worksheet for a given PID55//export function get_sage_path(pid) {}56// return path57// }5859/*60Sage Session object6162Until you actually try to call it no socket need63*/64class SageSession implements ISageSession {65private _path: string;66private _client: Client;67private _output_cb: {68[key: string]: CB<{ done: boolean; error: string }, any>;69} = {};70private _socket: CoCalcSocket | undefined;71public init_socket: () => Promise<void>;7273constructor(opts: Readonly<SageSessionOpts>) {74this.dbg = this.dbg.bind(this);75this.close = this.close.bind(this);76this.is_running = this.is_running.bind(this);77this._init_socket = this._init_socket.bind(this);78this.init_socket = reuseInFlight(this._init_socket).bind(this);79this._init_path = this._init_path.bind(this);80this.call = this.call.bind(this);81this._handle_mesg_blob = this._handle_mesg_blob.bind(this);82this._handle_mesg_json = this._handle_mesg_json.bind(this);83this.dbg("constructor")();84this._path = opts.path;85this._client = opts.client;86this._output_cb = {};87}8889private dbg(f: string) {90return (m?: string) =>91winston.debug(`SageSession(path='${this._path}').${f}: ${m}`);92}9394public close(): void {95if (this._socket != null) {96const pid = this._socket.pid;97if (pid != null) processKill(pid, 9);98}99this._socket?.end();100delete this._socket;101for (let id in this._output_cb) {102const cb = this._output_cb[id];103cb({ done: true, error: "killed" });104}105this._output_cb = {};106delete cache[this._path];107}108109// return true if there is a socket connection to a sage server process110is_running(): boolean {111return this._socket != null;112}113114// NOTE: There can be many simultaneous init_socket calls at the same time,115// if e.g., the socket doesn't exist and there are a bunch of calls to @call116// at the same time.117// See https://github.com/sagemathinc/cocalc/issues/3506118// wrapped in reuseInFlight !119private async _init_socket(): Promise<void> {120const dbg = this.dbg("init_socket()");121dbg();122try {123const socket: CoCalcSocket = await get_sage_socket();124125dbg("successfully opened a sage session");126this._socket = socket;127128socket.on("end", () => {129delete this._socket;130return dbg("codemirror session terminated");131});132133// CRITICAL: we must define this handler before @_init_path below,134// or @_init_path can't possibly work... since it would wait for135// this handler to get the response message!136socket.on("mesg", (type: TCPMesgType, mesg: TCPMessage) => {137dbg(`sage session: received message ${type}`);138switch (type) {139case "json":140this._handle_mesg_json(mesg);141break;142case "blob":143this._handle_mesg_blob(mesg);144break;145}146});147148await this._init_path();149} catch (err) {150if (err) {151dbg(`fail -- ${err}.`);152throw err;153}154}155}156157private async _init_path(): Promise<void> {158const dbg = this.dbg("_init_path()");159dbg();160return new Promise<void>((resolve, reject) => {161this.call({162input: {163event: "execute_code",164code: "os.chdir(salvus.data['path']);__file__=salvus.data['file']",165data: {166path: abspath(path_split(this._path).head),167file: abspath(this._path),168},169preparse: false,170},171cb: (resp) => {172let err: string | undefined = undefined;173if (resp.stderr) {174err = resp.stderr;175dbg(`error '${err}'`);176}177if (resp.done) {178if (err) {179reject(err);180} else {181resolve();182}183}184},185});186});187}188189public async call({ input, cb }: Readonly<SageCallOpts>): Promise<void> {190const dbg = this.dbg("call");191dbg(`input='${trunc(to_json(input), 300)}'`);192switch (input.event) {193case "ping":194cb({ pong: true });195return;196197case "status":198cb({ running: this.is_running() });199return;200201case "signal":202if (this._socket != null) {203dbg(`sending signal ${input.signal} to process ${this._socket.pid}`);204const pid = this._socket.pid;205if (pid != null) processKill(pid, input.signal);206}207cb({});208return;209210case "restart":211dbg("restarting sage session");212if (this._socket != null) {213this.close();214}215try {216await this.init_socket();217cb({});218} catch (err) {219cb({ error: err });220}221return;222223case "raw_input":224dbg("sending sage_raw_input event");225this._socket?.write_mesg("json", {226event: "sage_raw_input",227value: input.value,228});229return;230231default:232// send message over socket and get responses233try {234if (this._socket == null) {235await this.init_socket();236}237238if (input.id == null) {239input.id = uuid();240dbg(`generated new random uuid for input: '${input.id}' `);241}242243if (this._socket == null) {244throw new Error("no socket");245}246247this._socket.write_mesg("json", input);248249this._output_cb[input.id] = cb; // this is when opts.cb will get called...250} catch (err) {251cb({ done: true, error: err });252}253}254}255private _handle_mesg_blob(mesg: TCPMessage) {256const { uuid } = mesg;257let { blob } = mesg;258const dbg = this.dbg(`_handle_mesg_blob(uuid='${uuid}')`);259dbg();260261if (blob == null) {262dbg("no blob -- dropping message");263return;264}265266// This should never happen, typing enforces this to be a Buffer267if (typeof blob === "string") {268dbg("blob is string -- converting to buffer");269blob = Buffer.from(blob, "utf8");270}271272this._client.save_blob({273blob,274uuid,275cb: (err, resp) => {276if (err) {277resp = message.save_blob({278error: err,279sha1: uuid, // dumb - that sha1 should be called uuid...280});281}282this._socket?.write_mesg("json", resp);283},284});285}286287private _handle_mesg_json(mesg: TCPMessage) {288const dbg = this.dbg("_handle_mesg_json");289dbg(`mesg='${trunc_middle(to_json(mesg), 400)}'`);290if (mesg == null) return; // should not happen291const { id } = mesg;292if (id == null) return; // should not happen293const cb = this._output_cb[id];294if (cb != null) {295// Must do this check first since it uses done:false.296if (mesg.done || mesg.done == null) {297delete this._output_cb[id];298mesg.done = true;299}300if (mesg.done != null && !mesg.done) {301// waste of space to include done part of mesg if just false for everything else...302delete mesg.done;303}304cb(mesg);305}306}307}308309310