Path: blob/master/src/packages/project/conat/terminal/manager.ts
1450 views
import { reuseInFlight } from "@cocalc/util/reuse-in-flight";1import { getLogger } from "@cocalc/project/logger";2import {3createTerminalServer,4type ConatService,5} from "@cocalc/conat/service/terminal";6import { project_id, compute_server_id } from "@cocalc/project/data";7import { isEqual } from "lodash";8import ensureContainingDirectoryExists from "@cocalc/backend/misc/ensure-containing-directory-exists";9import { Session } from "./session";10import {11computeServerManager,12ComputeServerManager,13} from "@cocalc/conat/compute/manager";14const logger = getLogger("project:conat:terminal:manager");15import type { CreateTerminalOptions } from "@cocalc/conat/project/api/editor";1617let manager: TerminalManager | null = null;18export const createTerminalService = async (19termPath: string,20opts?: CreateTerminalOptions,21) => {22if (manager == null) {23logger.debug("createTerminalService -- creating manager");24manager = new TerminalManager();25}26return await manager.createTerminalService(termPath, opts);27};2829export function pidToPath(pid: number): string | undefined {30return manager?.pidToPath(pid);31}3233export class TerminalManager {34private services: { [termPath: string]: ConatService } = {};35private sessions: { [termPath: string]: Session } = {};36private computeServers?: ComputeServerManager;3738constructor() {39this.computeServers = computeServerManager({ project_id });40this.computeServers.on("change", this.handleComputeServersChange);41}4243private handleComputeServersChange = async ({ path: termPath, id = 0 }) => {44const service = this.services[termPath];45if (service == null) return;46if (id != compute_server_id) {47logger.debug(48`terminal '${termPath}' moved: ${compute_server_id} --> ${id}: Stopping`,49);50this.sessions[termPath]?.close();51service.close();52delete this.services[termPath];53delete this.sessions[termPath];54}55};5657close = () => {58logger.debug("close");59if (this.computeServers == null) {60return;61}62for (const termPath in this.services) {63this.services[termPath].close();64}65this.services = {};66this.sessions = {};67this.computeServers.removeListener(68"change",69this.handleComputeServersChange,70);71this.computeServers.close();72delete this.computeServers;73};7475private getSession = async (76termPath: string,77options,78noCreate?: boolean,79): Promise<Session> => {80const cur = this.sessions[termPath];81if (cur != null) {82return cur;83}84if (noCreate) {85throw Error("no terminal session");86}87await this.createTerminal({ ...options, termPath });88const session = this.sessions[termPath];89if (session == null) {90throw Error(91`BUG: failed to create terminal session - ${termPath} (this should not happen)`,92);93}94return session;95};9697createTerminalService = reuseInFlight(98async (termPath: string, opts?: CreateTerminalOptions) => {99if (this.services[termPath] != null) {100return;101}102let options: any = undefined;103104const getSession = async (options, noCreate?) =>105await this.getSession(termPath, options, noCreate);106107const impl = {108create: async (109opts: CreateTerminalOptions,110): Promise<{ success: "ok"; note?: string; ephemeral?: boolean }> => {111// save options to reuse.112options = opts;113const note = await this.createTerminal({ ...opts, termPath });114return { success: "ok", note };115},116117write: async (data: string): Promise<void> => {118// logger.debug("received data", data.length);119if (typeof data != "string") {120throw Error(`data must be a string -- ${JSON.stringify(data)}`);121}122const session = await getSession(options);123await session.write(data);124},125126restart: async () => {127const session = await getSession(options);128await session.restart();129},130131cwd: async () => {132const session = await getSession(options);133return await session.getCwd();134},135136kill: async () => {137try {138const session = await getSession(options, true);139await session.kill();140session.close();141} catch {142return;143}144},145146size: async (opts: {147rows: number;148cols: number;149browser_id: string;150kick?: boolean;151}) => {152const session = await getSession(options);153session.setSize(opts);154},155156close: async (browser_id: string) => {157this.sessions[termPath]?.browserLeaving(browser_id);158},159};160161const server = createTerminalServer({ termPath, project_id, impl });162163server.on("close", () => {164this.sessions[termPath]?.close();165delete this.sessions[termPath];166delete this.services[termPath];167});168169this.services[termPath] = server;170171if (opts != null) {172await impl.create(opts);173}174},175);176177closeTerminal = (termPath: string) => {178const cur = this.sessions[termPath];179if (cur != null) {180cur.close();181delete this.sessions[termPath];182}183};184185createTerminal = reuseInFlight(186async (params) => {187if (params == null) {188throw Error("params must be specified");189}190const { termPath, ...options } = params;191if (!termPath) {192throw Error("termPath must be specified");193}194await ensureContainingDirectoryExists(termPath);195let note = "";196const cur = this.sessions[termPath];197if (cur != null) {198if (!isEqual(cur.options, options) || cur.state == "closed") {199// clean up -- we will make new one below200this.closeTerminal(termPath);201note += "Closed existing session. ";202} else {203// already have a working session with correct options204note += "Already have working session with same options. ";205return note;206}207}208note += "Creating new session.";209let session = new Session({ termPath, options });210await session.init();211if (session.state == "closed") {212// closed during init -- unlikely but possible; try one more time213session = new Session({ termPath, options });214await session.init();215if (session.state == "closed") {216throw Error(`unable to create terminal session for ${termPath}`);217}218} else {219this.sessions[termPath] = session;220return note;221}222},223{224createKey: (args) => {225return args[0]?.termPath ?? "";226},227},228);229230pidToPath = (pid: number): string | undefined => {231for (const termPath in this.sessions) {232const s = this.sessions[termPath];233if (s.pid == pid) {234return s.options.path;235}236}237};238}239240241