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