riju/backend/src/api.ts

173 lines
4.3 KiB
TypeScript

"use strict";
import * as fs from "fs";
import * as pty from "node-pty";
import { IPty } from "node-pty";
import * as path from "path";
import * as tmp from "tmp";
import * as WebSocket from "ws";
import { LangConfig, langs } from "./langs";
export class Session {
code: string;
config: LangConfig;
term: { pty: IPty; live: boolean };
ws: WebSocket;
constructor(ws: WebSocket, lang: string) {
this.ws = ws;
this.config = langs[lang];
this.term = { pty: null, live: false };
this.code = "";
try {
this.ws.send(
JSON.stringify({
event: "setMonacoLanguage",
monacoLanguage: this.config.monacoLang,
})
);
} catch (err) {
//
}
if (this.config.template) {
try {
this.ws.send(
JSON.stringify({
event: "insertTemplate",
template: this.config.template,
})
);
} catch (err) {
//
}
}
this.run().catch(console.error);
ws.on("message", this.handleClientMessage);
}
handleClientMessage = (event: string) => {
let msg: any;
try {
msg = JSON.parse(event);
} catch (err) {
console.error(`failed to parse client message: ${msg}`);
return;
}
switch (msg?.event) {
case "terminalInput":
if (!this.term) {
console.error(`terminalInput: no terminal`);
} else if (typeof msg.input !== "string") {
console.error(`terminalInput: missing or malformed input field`);
} else {
this.term.pty.write(msg.input);
}
break;
case "runCode":
if (typeof msg.code !== "string") {
console.error(`runCode: missing or malformed code field`);
} else {
this.code = msg.code;
this.run();
}
break;
default:
console.error(`unknown client message type: ${msg.event}`);
break;
}
};
run = async () => {
const { repl, main, suffix, compile, run, hacks } = this.config;
if (this.term.pty) {
this.term.pty.kill();
this.term.live = false;
}
try {
this.ws.send(JSON.stringify({ event: "terminalClear" }));
} catch (err) {
//
}
const tmpdir: string = await new Promise((resolve, reject) =>
tmp.dir({ unsafeCleanup: true }, (err, path) => {
if (err) {
reject(err);
} else {
resolve(path);
}
})
);
let cmdline: string;
if (!run) {
cmdline = `echo 'Support for ${this.config.name} is not yet implemented.'`;
} else if (this.code) {
let code = this.code;
if (suffix) {
code += suffix;
}
await new Promise((resolve, reject) =>
fs.writeFile(path.resolve(tmpdir, main), code, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
})
);
cmdline = run;
if (compile) {
cmdline = compile + " && " + run;
}
} else if (repl) {
cmdline = repl;
} else {
return;
}
if (hacks && hacks.includes("ghci-config") && run) {
if (this.code) {
const contents = ":load Main\nmain\n";
await new Promise((resolve, reject) => {
fs.writeFile(path.resolve(tmpdir, ".ghci"), contents, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
} else {
await new Promise((resolve, reject) =>
fs.unlink(path.resolve(tmpdir, ".ghci"), (err) => {
if (err && err.code !== "ENOENT") {
reject(err);
} else {
resolve();
}
})
);
}
}
const term = {
pty: pty.spawn("bash", ["-c", cmdline], {
name: "xterm-color",
cwd: tmpdir,
env: process.env,
}),
live: true,
};
this.term = term;
term.pty.on("data", (data) => {
// Capture term in closure so that we don't keep sending output
// from the old pty even after it's been killed (see ghci).
if (term.live) {
try {
this.ws.send(
JSON.stringify({ event: "terminalOutput", output: data })
);
} catch (err) {
//
}
}
});
};
}