Refactor api.ts, plug resource leaks, fix build

This commit is contained in:
Radon Rosborough 2020-07-11 11:00:17 -06:00
parent bb14a6c9ab
commit 0b8d5d244d
17 changed files with 888 additions and 359 deletions

View File

@ -2,6 +2,8 @@ FROM ubuntu:focal
ARG UID ARG UID
COPY scripts/my_init /usr/bin/my_init
COPY scripts/docker-install-phase0.bash /tmp/ COPY scripts/docker-install-phase0.bash /tmp/
RUN /tmp/docker-install-phase0.bash RUN /tmp/docker-install-phase0.bash
@ -44,6 +46,6 @@ RUN chmod go-rwx /home/docker
EXPOSE 6119 EXPOSE 6119
EXPOSE 6120 EXPOSE 6120
ENTRYPOINT ["/usr/local/bin/pid1.bash"] ENTRYPOINT ["/usr/bin/my_init", "/usr/local/bin/pid1.bash"]
COPY scripts/pid1.bash /usr/local/bin/ COPY scripts/pid1.bash /usr/local/bin/
CMD ["bash"] CMD ["bash"]

View File

@ -4,6 +4,8 @@ FROM ubuntu:focal
# prod, it's not actually read by anything. # prod, it's not actually read by anything.
ARG UID ARG UID
COPY scripts/my_init /usr/bin/my_init
COPY scripts/docker-install-phase0.bash /tmp/ COPY scripts/docker-install-phase0.bash /tmp/
RUN /tmp/docker-install-phase0.bash RUN /tmp/docker-install-phase0.bash
@ -46,7 +48,7 @@ RUN chmod go-rwx /home/docker
EXPOSE 6119 EXPOSE 6119
EXPOSE 6120 EXPOSE 6120
ENTRYPOINT ["/usr/local/bin/pid1.bash"] ENTRYPOINT ["/usr/bin/my_init", "/usr/local/bin/pid1.bash"]
COPY scripts/pid1.bash /usr/local/bin/ COPY scripts/pid1.bash /usr/local/bin/
CMD ["yarn", "run", "server"] CMD ["yarn", "run", "server"]

View File

@ -43,8 +43,9 @@ container first:
$ make docker $ make docker
Note that building the image takes about 30 minutes on high-end Note that building the image can take up to 45 minutes even on
hardware and ethernet, and it requires about 15 GB of disk space. high-end hardware and internet, and it requires about 15 GB of disk
space.
## Flag ## Flag

View File

@ -9,141 +9,272 @@ import { v4 as getUUID } from "uuid";
import { LangConfig, langs } from "./langs"; import { LangConfig, langs } from "./langs";
import { borrowUser } from "./users"; import { borrowUser } from "./users";
import { import * as util from "./util";
callPrivileged, import { Context, Options, bash } from "./util";
getEnv,
rijuSystemPrivileged, const allSessions: Set<Session> = new Set();
spawnPrivileged,
} from "./util";
export class Session { export class Session {
ws: WebSocket;
uuid: string; uuid: string;
code: string | null; lang: string;
config: LangConfig;
term: { pty: IPty | null; live: boolean }; tearingDown: boolean = false;
// Initialized by setup()
uidInfo: {
uid: number;
returnUID: () => Promise<void>;
} | null = null;
// Initialized later or never
term: { pty: IPty; live: boolean } | null = null;
lsp: { lsp: {
proc: ChildProcess; proc: ChildProcess;
reader: rpc.StreamMessageReader; reader: rpc.StreamMessageReader;
writer: rpc.StreamMessageWriter; writer: rpc.StreamMessageWriter;
} | null; } | null = null;
daemon: ChildProcess | null; daemon: { proc: ChildProcess } | null = null;
ws: WebSocket;
homedir: string | null; get homedir() {
uid: number | null; return `/tmp/riju/${this.uuid}`;
uidCleanup: (() => Promise<void>) | null; }
get config() {
return langs[this.lang];
}
get uid() {
return this.uidInfo!.uid;
}
returnUID = async () => {
this.uidInfo && (await this.uidInfo.returnUID());
};
get context() {
return { uid: this.uid, uuid: this.uuid };
}
get env() {
return util.getEnv(this.uuid);
}
log = (msg: string) => console.log(`[${this.uuid}] ${msg}`); log = (msg: string) => console.log(`[${this.uuid}] ${msg}`);
constructor(ws: WebSocket, lang: string) { constructor(ws: WebSocket, lang: string) {
this.uuid = getUUID();
this.log(`Creating session, language ${lang}`);
this.ws = ws; this.ws = ws;
this.config = langs[lang]; this.uuid = getUUID();
this.term = { pty: null, live: false }; this.lang = lang;
this.lsp = null; this.log(`Creating session, language ${this.lang}`);
this.daemon = null; this.setup();
this.code = null; }
this.homedir = null;
this.uid = null; run = async (args: string[], options?: Options) => {
this.uidCleanup = null; return await util.run(args, this.log, options);
ws.on("message", this.handleClientMessage); };
ws.on("close", () =>
this.cleanup().catch((err) => { privilegedSetup = () => util.privilegedSetup(this.context);
this.log(`Error during session cleanup`); privilegedSpawn = (args: string[]) =>
console.log(err); util.privilegedSpawn(this.context, args);
privilegedUseradd = () => util.privilegedUseradd(this.uid);
privilegedTeardown = () => util.privilegedTeardown(this.context);
setup = async () => {
try {
allSessions.add(this);
const { uid, returnUID } = await borrowUser(this.log);
this.uidInfo = { uid, returnUID };
this.log(`Borrowed uid ${this.uid}`);
await this.run(this.privilegedSetup());
await this.runCode();
if (this.config.daemon) {
const daemonArgs = this.privilegedSpawn(bash(this.config.daemon));
const daemonProc = spawn(daemonArgs[0], daemonArgs.slice(1), {
env: this.env,
});
this.daemon = {
proc: daemonProc,
};
for (const stream of [daemonProc.stdout, daemonProc.stderr]) {
stream.on("data", (data) =>
this.send({
event: "serviceLog",
service: "daemon",
output: data.toString("utf8"),
}) })
); );
this.run().catch((err) => { daemonProc.on("exit", (code, signal) =>
this.log(`Error while setting up environment for pty`);
console.log(err);
this.send({ event: "terminalClear" });
this.send({ this.send({
event: "serviceFailed",
service: "daemon",
error: `Exited with status ${signal || code}`,
})
);
daemonProc.on("error", (err) =>
this.send({
event: "serviceFailed",
service: "daemon",
error: `${err}`,
})
);
}
}
if (this.config.lsp) {
if (this.config.lspSetup) {
await this.run(this.privilegedSpawn(bash(this.config.lspSetup)));
}
const lspArgs = this.privilegedSpawn(bash(this.config.lsp));
const lspProc = spawn(lspArgs[0], lspArgs.slice(1), { env: this.env });
this.lsp = {
proc: lspProc,
reader: new rpc.StreamMessageReader(lspProc.stdout),
writer: new rpc.StreamMessageWriter(lspProc.stdin),
};
this.lsp.reader.listen((data: any) => {
this.send({ event: "lspOutput", output: data });
});
lspProc.stderr.on("data", (data) =>
this.send({
event: "serviceLog",
service: "lsp",
output: data.toString("utf8"),
})
);
lspProc.on("exit", (code, signal) =>
this.send({
event: "serviceFailed",
service: "lsp",
error: `Exited with status ${signal || code}`,
})
);
lspProc.on("error", (err) =>
this.send({ event: "serviceFailed", service: "lsp", error: `${err}` })
);
this.send({ event: "lspStarted", root: this.homedir });
}
this.ws.on("message", this.receive);
this.ws.on("close", async () => {
await this.teardown();
});
this.ws.on("error", async (err) => {
this.log(`Websocket error: ${err}`);
await this.teardown();
});
} catch (err) {
this.log(`Error while setting up environment`);
console.log(err);
this.sendError(err);
await this.teardown();
}
};
send = async (msg: any) => {
try {
if (this.tearingDown) {
return;
}
this.ws.send(JSON.stringify(msg));
} catch (err) {
this.log(`Failed to send websocket message: ${err}`);
await this.teardown();
}
};
sendError = async (err: any) => {
await this.send({ event: "terminalClear" });
await this.send({
event: "terminalOutput", event: "terminalOutput",
output: `Riju encountered an unexpected error: ${err} output: `Riju encountered an unexpected error: ${err}
\r
\rYou may want to save your code and refresh the page. \rYou may want to save your code and refresh the page.
`, `,
}); });
});
}
send = (msg: any) => {
try {
this.ws.send(JSON.stringify(msg));
} catch (err) {
//
}
}; };
handleClientMessage = (event: string) => {
logBadMessage = (msg: any) => {
this.log(`Got malformed message from client: ${msg}`);
};
receive = async (event: string) => {
try {
if (this.tearingDown) {
return;
}
let msg: any; let msg: any;
try { try {
msg = JSON.parse(event); msg = JSON.parse(event);
} catch (err) { } catch (err) {
this.log(`Failed to parse client message: ${msg}`); this.log(`Failed to parse message from client: ${msg}`);
return; return;
} }
switch (msg?.event) { switch (msg && msg.event) {
case "terminalInput": case "terminalInput":
if (!this.term) { if (typeof msg.input !== "string") {
this.log(`Got terminal input before pty was started`); this.logBadMessage(msg);
} else if (typeof msg.input !== "string") { break;
this.log(`Got malformed terminal input message`);
} else {
this.term.pty!.write(msg.input);
} }
if (!this.term) {
this.log("terminalInput ignored because term is null");
break;
}
this.term!.pty.write(msg.input);
break; break;
case "runCode": case "runCode":
if (typeof msg.code !== "string") { if (typeof msg.code !== "string") {
this.log(`Got malformed run message`); this.logBadMessage(msg);
} else { break;
this.code = msg.code;
this.run();
} }
await this.runCode(msg.code);
break; break;
case "lspInput": case "lspInput":
if (!this.lsp) { if (typeof msg.input !== "object" || !msg) {
this.log(`Got LSP input before language server was started`); this.logBadMessage(msg);
} else { break;
this.lsp.writer.write(msg.input);
} }
if (!this.lsp) {
this.log(`lspInput ignored because lsp is null`);
break;
}
this.lsp.writer.write(msg.input);
break; break;
default: default:
this.log(`Got unknown message type: ${msg.event}`); this.logBadMessage(msg);
break; break;
} }
}; } catch (err) {
run = async () => { this.log(`Error while handling message from client`);
if (this.uid === null) { console.log(err);
({ uid: this.uid, cleanup: this.uidCleanup } = await borrowUser( this.sendError(err);
this.log
));
this.log(`Borrowed uid ${this.uid}`);
} }
};
runCode = async (code?: string) => {
try {
const { const {
name, name,
daemon,
repl, repl,
main, main,
suffix, suffix,
createEmpty, createEmpty,
compile, compile,
run, run,
lspSetup,
lsp,
template, template,
hacks, hacks,
} = this.config; } = this.config;
if (this.term.pty) { if (this.term) {
this.term.pty.kill(); const pid = this.term.pty.pid;
const args = this.privilegedSpawn(
bash(`kill -SIGTERM ${pid}; sleep 3; kill -SIGKILL ${pid}`)
);
spawn(args[0], args.slice(1), { env: this.env });
// Signal to terminalOutput message generator using closure.
this.term.live = false; this.term.live = false;
this.term = null;
} }
this.send({ event: "terminalClear" }); this.send({ event: "terminalClear" });
if (this.homedir == null) {
this.homedir = `/tmp/riju/${this.uuid}`;
await callPrivileged(["setup", `${this.uid}`, this.uuid], this.log);
}
let cmdline: string; let cmdline: string;
if (!run) { if (code) {
cmdline = `echo 'Support for ${this.config.name} is not yet implemented.'`;
} else if (this.code) {
cmdline = run; cmdline = run;
if (compile) { if (compile) {
cmdline = `( ${compile} ) && ( ${run} )`; cmdline = `( ${compile} ) && ( ${run} )`;
@ -153,144 +284,79 @@ export class Session {
} else { } else {
cmdline = `echo '${name} has no REPL, press Run to see it in action'`; cmdline = `echo '${name} has no REPL, press Run to see it in action'`;
} }
let code = this.code; if (code === undefined) {
if (this.code === null) {
code = createEmpty ? "" : template; code = createEmpty ? "" : template;
} }
if (code && suffix) { if (code && suffix) {
code += suffix; code += suffix;
} }
if (main.includes("/")) { if (main.includes("/")) {
await spawnPrivileged( await this.run(
this.uid, this.privilegedSpawn([
this.uuid, "mkdir",
["mkdir", "-p", path.dirname(path.resolve(this.homedir, main))], "-p",
this.log path.dirname(`${this.homedir}/${main}`),
])
); );
} }
await spawnPrivileged( await this.run(
this.uid, this.privilegedSpawn([
this.uuid, "sh",
["sh", "-c", `cat > ${path.resolve(this.homedir, main)}`], "-c",
this.log, `cat > ${path.resolve(this.homedir, main)}`,
{ input: code as string } ]),
{ input: code }
); );
if (hacks && hacks.includes("ghci-config") && run) { if (hacks && hacks.includes("ghci-config") && run) {
if (this.code) { if (code) {
const contents = ":load Main\nmain\n"; await this.run(
await spawnPrivileged( this.privilegedSpawn(["sh", "-c", `cat > ${this.homedir}/.ghci`]),
this.uid, { input: ":load Main\nmain\n" }
this.uuid,
["sh", "-c", `cat > ${path.resolve(this.homedir, ".ghci")}`],
this.log,
{ input: contents }
); );
} else { } else {
await spawnPrivileged( await this.run(
this.uid, this.privilegedSpawn(["rm", "-f", `${this.homedir}/.ghci`])
this.uuid,
["rm", "-f", path.resolve(this.homedir, ".ghci")],
this.log
); );
} }
} }
const args = [ const termArgs = this.privilegedSpawn(bash(cmdline));
rijuSystemPrivileged,
"spawn",
`${this.uid}`,
`${this.uuid}`,
"bash",
"-c",
cmdline,
];
const env = getEnv(this.uuid);
const term = { const term = {
pty: pty.spawn(args[0], args.slice(1), { pty: pty.spawn(termArgs[0], termArgs.slice(1), {
name: "xterm-color", name: "xterm-color",
env, env: this.env,
}), }),
live: true, live: true,
}; };
this.term = term; this.term = term;
term.pty.on("data", (data) => { this.term.pty.on("data", (data) => {
// Capture term in closure so that we don't keep sending output // Capture term in closure so that we don't keep sending output
// from the old pty even after it's been killed (see ghci). // from the old pty even after it's been killed (see ghci).
if (term.live) { if (term.live) {
this.send({ event: "terminalOutput", output: data }); this.send({ event: "terminalOutput", output: data });
} }
}); });
if (daemon && this.daemon === null) { } catch (err) {
this.daemon = spawn("bash", ["-c", daemon], { env: getEnv(this.uuid) }); this.log(`Error while running user code`);
this.daemon.on("exit", (code) => console.log(err);
this.send({ event: "daemonCrashed", code }) this.sendError(err);
);
this.daemon.stdout!.on("data", (data) =>
this.send({ event: "daemonLog", output: data.toString("utf8") })
);
this.daemon.stderr!.on("data", (data) =>
this.send({ event: "daemonLog", output: data.toString("utf8") })
);
}
if (lsp && this.lsp === null) {
if (lspSetup) {
await spawnPrivileged(
this.uid!,
this.uuid,
["bash", "-c", lspSetup],
this.log
);
}
const lspArgs = [
rijuSystemPrivileged,
"spawn",
`${this.uid}`,
`${this.uuid}`,
"bash",
"-c",
lsp,
];
const proc = spawn(lspArgs[0], lspArgs.slice(1), {
env: getEnv(this.uuid),
});
proc.on("exit", (code) => this.send({ event: "lspCrashed", code }));
proc.stderr.on("data", (data) =>
this.send({ event: "lspLog", output: data.toString("utf8") })
);
this.lsp = {
proc,
reader: new rpc.StreamMessageReader(proc.stdout),
writer: new rpc.StreamMessageWriter(proc.stdin),
};
this.lsp.reader.listen((data) => {
this.send({ event: "lspOutput", output: data });
});
this.send({ event: "lspStarted", root: `/tmp/riju/${this.uuid}` });
} }
}; };
cleanup = async () => {
this.log(`Cleaning up session`); teardown = async () => {
if (this.term.pty) { try {
await spawnPrivileged( if (this.tearingDown) {
this.uid!, return;
this.uuid,
["bash", "-c", `kill -9 ${this.term.pty.pid} 2>/dev/null || true`],
this.log
);
} }
if (this.lsp !== null) { this.log(`Tearing down session`);
await spawnPrivileged( this.tearingDown = true;
this.uid!, allSessions.delete(this);
this.uuid, await new Promise((resolve) => setTimeout(resolve, 5000));
["bash", "-c", `kill -9 ${this.lsp.proc.pid} 2>/dev/null || true`], await this.run(this.privilegedTeardown());
this.log await this.returnUID();
); this.ws.terminate();
} } catch (err) {
if (this.homedir) { this.log(`Error during teardown`);
await callPrivileged(["teardown", this.uuid], this.log); console.log(err);
}
if (this.uidCleanup) {
await this.uidCleanup();
this.log(`Returned uid ${this.uid}`);
} }
}; };
} }

View File

@ -5,7 +5,13 @@ import { v4 as getUUID } from "uuid";
import { langs } from "./langs"; import { langs } from "./langs";
import { borrowUser } from "./users"; import { borrowUser } from "./users";
import { callPrivileged, getEnv, rijuSystemPrivileged } from "./util"; import {
getEnv,
privilegedSetup,
privilegedSpawn,
privilegedTeardown,
run,
} from "./util";
function die(msg: any) { function die(msg: any) {
console.error(msg); console.error(msg);
@ -18,9 +24,9 @@ function log(msg: any) {
async function main() { async function main() {
const uuid = getUUID(); const uuid = getUUID();
const { uid, cleanup } = await borrowUser(log); const { uid, returnUID } = await borrowUser(log);
await callPrivileged(["setup", `${uid}`, uuid], log); await run(privilegedSetup({ uid, uuid }), log);
const args = [rijuSystemPrivileged, "spawn", `${uid}`, `${uuid}`, "bash"]; const args = privilegedSpawn({ uid, uuid }, ["bash"]);
const proc = spawn(args[0], args.slice(1), { const proc = spawn(args[0], args.slice(1), {
env: getEnv(uuid), env: getEnv(uuid),
stdio: "inherit", stdio: "inherit",
@ -29,7 +35,8 @@ async function main() {
proc.on("error", reject); proc.on("error", reject);
proc.on("exit", resolve); proc.on("exit", resolve);
}); });
await cleanup(); await run(privilegedTeardown({ uid, uuid }), log);
await returnUID();
} }
main().catch(die); main().catch(die);

View File

@ -100,7 +100,7 @@ if (useTLS) {
httpsServer.listen(tlsPort, host, () => httpsServer.listen(tlsPort, host, () =>
console.log(`Listening on https://${host}:${tlsPort}`) console.log(`Listening on https://${host}:${tlsPort}`)
); );
http const server = http
.createServer((req, res) => { .createServer((req, res) => {
res.writeHead(301, { res.writeHead(301, {
Location: "https://" + req.headers["host"] + req.url, Location: "https://" + req.headers["host"] + req.url,
@ -112,7 +112,7 @@ if (useTLS) {
); );
} else { } else {
addWebsocket(app, undefined); addWebsocket(app, undefined);
app.listen(port, host, () => const server = app.listen(port, host, () =>
console.log(`Listening on http://${host}:${port}`) console.log(`Listening on http://${host}:${port}`)
); );
} }

View File

@ -7,7 +7,7 @@ import * as _ from "lodash";
import * as parsePasswd from "parse-passwd"; import * as parsePasswd from "parse-passwd";
import { PRIVILEGED } from "./config"; import { PRIVILEGED } from "./config";
import { callPrivileged } from "./util"; import { privilegedUseradd, run } from "./util";
// Keep in sync with system/src/riju-system-privileged.c // Keep in sync with system/src/riju-system-privileged.c
const MIN_UID = 2000; const MIN_UID = 2000;
@ -21,7 +21,7 @@ let lock = new AsyncLock();
async function readExistingUsers(log: (msg: string) => void) { async function readExistingUsers(log: (msg: string) => void) {
availIds = parsePasswd( availIds = parsePasswd(
await new Promise((resolve, reject) => await new Promise((resolve: (result: string) => void, reject) =>
fs.readFile("/etc/passwd", "utf-8", (err, data) => { fs.readFile("/etc/passwd", "utf-8", (err, data) => {
if (err) { if (err) {
reject(err); reject(err);
@ -43,7 +43,7 @@ async function createUser(log: (msg: string) => void): Promise<number> {
throw new Error("too many users"); throw new Error("too many users");
} }
const uid = nextId!; const uid = nextId!;
await callPrivileged(["useradd", `${uid}`], log); await run(privilegedUseradd(uid), log);
log(`Created new user with ID ${uid}`); log(`Created new user with ID ${uid}`);
nextId! += 1; nextId! += 1;
return uid; return uid;
@ -51,7 +51,7 @@ async function createUser(log: (msg: string) => void): Promise<number> {
export async function borrowUser(log: (msg: string) => void) { export async function borrowUser(log: (msg: string) => void) {
if (!PRIVILEGED) { if (!PRIVILEGED) {
return { uid: CUR_UID, cleanup: async () => {} }; return { uid: CUR_UID, returnUID: async () => {} };
} else { } else {
return await lock.acquire("key", async () => { return await lock.acquire("key", async () => {
if (availIds === null || nextId === null) { if (availIds === null || nextId === null) {
@ -65,7 +65,7 @@ export async function borrowUser(log: (msg: string) => void) {
} }
return { return {
uid, uid,
cleanup: async () => { returnUID: async () => {
await lock.acquire("key", () => { await lock.acquire("key", () => {
availIds!.push(uid); availIds!.push(uid);
}); });

View File

@ -3,11 +3,16 @@ import * as process from "process";
import * as appRoot from "app-root-path"; import * as appRoot from "app-root-path";
interface Options extends SpawnOptions { export interface Options extends SpawnOptions {
input?: string; input?: string;
check?: boolean; check?: boolean;
} }
export interface Context {
uid: number;
uuid: string;
}
export const rijuSystemPrivileged = appRoot.resolve( export const rijuSystemPrivileged = appRoot.resolve(
"system/out/riju-system-privileged" "system/out/riju-system-privileged"
); );
@ -26,7 +31,7 @@ export function getEnv(uuid: string) {
}; };
} }
export async function call( export async function run(
args: string[], args: string[],
log: (msg: string) => void, log: (msg: string) => void,
options?: Options options?: Options
@ -63,26 +68,22 @@ export async function call(
}); });
} }
export async function callPrivileged( export function privilegedUseradd(uid: number) {
args: string[], return [rijuSystemPrivileged, "useradd", `${uid}`];
log: (msg: string) => void,
options?: Options
) {
await call([rijuSystemPrivileged].concat(args), log, options);
} }
export async function spawnPrivileged( export function privilegedSetup({ uid, uuid }: Context) {
uid: number, return [rijuSystemPrivileged, "setup", `${uid}`, uuid];
uuid: string, }
args: string[],
log: (msg: string) => void, export function privilegedSpawn({ uid, uuid }: Context, args: string[]) {
options?: Options return [rijuSystemPrivileged, "spawn", `${uid}`, uuid].concat(args);
) { }
options = options || {};
options.env = getEnv(uuid); export function privilegedTeardown({ uid, uuid }: Context) {
await callPrivileged( return [rijuSystemPrivileged, "teardown", `${uid}`, uuid];
["spawn", `${uid}`, `${uuid}`].concat(args), }
log,
options export function bash(cmdline: string) {
); return ["bash", "-c", cmdline];
} }

View File

@ -66,12 +66,12 @@ class RijuMessageReader extends AbstractMessageReader {
} catch (err) { } catch (err) {
return; return;
} }
switch (message?.event) { switch (message && message.event) {
case "lspOutput": case "lspOutput":
if (DEBUG) { if (DEBUG) {
console.log("RECEIVE LSP:", message?.output); console.log("RECEIVE LSP:", message.output);
} }
this.callback!(message?.output); this.callback!(message.output);
break; break;
} }
} }
@ -135,14 +135,15 @@ async function main() {
if (DEBUG) { if (DEBUG) {
console.log("SEND", message); console.log("SEND", message);
} }
socket?.send(JSON.stringify(message)); if (socket) {
socket.send(JSON.stringify(message));
}
} }
function tryConnect() { function tryConnect() {
let clientDisposable: Disposable | null = null; let clientDisposable: Disposable | null = null;
let servicesDisposable: Disposable | null = null; let servicesDisposable: Disposable | null = null;
let lspLogBuffer = ""; const serviceLogBuffers: { [index: string]: string } = {};
let daemonLogBuffer = "";
console.log("Connecting to server..."); console.log("Connecting to server...");
socket = new WebSocket( socket = new WebSocket(
(document.location.protocol === "http:" ? "ws://" : "wss://") + (document.location.protocol === "http:" ? "ws://" : "wss://") +
@ -162,16 +163,17 @@ async function main() {
} }
if ( if (
DEBUG && DEBUG &&
message?.event !== "lspOutput" && message &&
message?.event !== "lspLog" && message.event !== "lspOutput" &&
message?.event !== "daemonLog" message.event !== "lspLog" &&
message.event !== "daemonLog"
) { ) {
console.log("RECEIVE:", message); console.log("RECEIVE:", message);
} }
if (message?.event && message?.event !== "error") { if (message && message.event && message.event !== "error") {
retryDelayMs = initialRetryDelayMs; retryDelayMs = initialRetryDelayMs;
} }
switch (message?.event) { switch (message && message.event) {
case "terminalClear": case "terminalClear":
term.reset(); term.reset();
return; return;
@ -208,7 +210,11 @@ async function main() {
documentSelector: [{ pattern: "**" }], documentSelector: [{ pattern: "**" }],
middleware: { middleware: {
workspace: { workspace: {
configuration: (params, token, configuration) => { configuration: (
params: any,
token: any,
configuration: any
) => {
return Array( return Array(
(configuration(params, token) as {}[]).length (configuration(params, token) as {}[]).length
).fill( ).fill(
@ -220,7 +226,7 @@ async function main() {
initializationOptions: config.lspInit || {}, initializationOptions: config.lspInit || {},
}, },
connectionProvider: { connectionProvider: {
get: (errorHandler, closeHandler) => get: (errorHandler: any, closeHandler: any) =>
Promise.resolve( Promise.resolve(
createConnection(connection, errorHandler, closeHandler) createConnection(connection, errorHandler, closeHandler)
), ),
@ -231,38 +237,27 @@ async function main() {
case "lspOutput": case "lspOutput":
// Should be handled by RijuMessageReader // Should be handled by RijuMessageReader
return; return;
case "lspLog": case "serviceLog":
if (typeof message.output !== "string") { if (
typeof message.service !== "string" ||
typeof message.output !== "string"
) {
console.error("Unexpected message from server:", message); console.error("Unexpected message from server:", message);
return; return;
} }
if (DEBUG) { if (DEBUG) {
lspLogBuffer += message.output; let buffer = serviceLogBuffers[message.service] || "";
while (lspLogBuffer.includes("\n")) { buffer += message.output;
const idx = lspLogBuffer.indexOf("\n"); while (buffer.includes("\n")) {
const line = lspLogBuffer.slice(0, idx); const idx = buffer.indexOf("\n");
lspLogBuffer = lspLogBuffer.slice(idx + 1); const line = buffer.slice(0, idx);
console.log(`LSP || ${line}`); buffer = buffer.slice(idx + 1);
console.log(`${message.service.toUpperCase()} || ${line}`);
} }
serviceLogBuffers[message.service] = buffer;
} }
return; return;
case "daemonLog": case "serviceCrashed":
if (typeof message.output !== "string") {
console.error("Unexpected message from server:", message);
return;
}
if (DEBUG) {
daemonLogBuffer += message.output;
while (daemonLogBuffer.includes("\n")) {
const idx = daemonLogBuffer.indexOf("\n");
const line = daemonLogBuffer.slice(0, idx);
daemonLogBuffer = daemonLogBuffer.slice(idx + 1);
console.log(`DAEMON || ${line}`);
}
}
return;
case "lspCrashed":
case "daemonCrashed":
return; return;
default: default:
console.error("Unexpected message from server:", message); console.error("Unexpected message from server:", message);

View File

@ -12,6 +12,7 @@
"@types/express-ws": "^3.0.0", "@types/express-ws": "^3.0.0",
"@types/lodash": "^4.14.155", "@types/lodash": "^4.14.155",
"@types/mkdirp": "^1.0.1", "@types/mkdirp": "^1.0.1",
"@types/node-cleanup": "^2.1.1",
"@types/parse-passwd": "^1.0.0", "@types/parse-passwd": "^1.0.0",
"@types/rimraf": "^3.0.0", "@types/rimraf": "^3.0.0",
"@types/shell-quote": "^1.7.0", "@types/shell-quote": "^1.7.0",
@ -30,7 +31,6 @@
"monaco-editor": "^0.20.0", "monaco-editor": "^0.20.0",
"monaco-editor-webpack-plugin": "^1.9.0", "monaco-editor-webpack-plugin": "^1.9.0",
"monaco-languageclient": "^0.13.0", "monaco-languageclient": "^0.13.0",
"node-cleanup": "^2.1.2",
"node-pty": "^0.9.0", "node-pty": "^0.9.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"parse-passwd": "^1.0.0", "parse-passwd": "^1.0.0",

View File

@ -36,7 +36,7 @@ wget -nv https://github.com/dhall-lang/dhall-haskell/releases/download/1.33.1/dh
mkdir dhall-json mkdir dhall-json
tar -xf dhall-json-*-x86_64-linux.tar.bz2 -C dhall-json tar -xf dhall-json-*-x86_64-linux.tar.bz2 -C dhall-json
mv dhall-json/bin/dhall-to-json dhall-json/bin/json-to-dhall /usr/bin/ mv dhall-json/bin/dhall-to-json dhall-json/bin/json-to-dhall /usr/bin/
rm dhall-json dhall-json-*-x86_64-linux.tar.bz2 rm -rf dhall-json dhall-json-*-x86_64-linux.tar.bz2
# Elixir # Elixir
wget -nv https://github.com/elixir-lsp/elixir-ls/releases/download/v0.5.0/elixir-ls.zip wget -nv https://github.com/elixir-lsp/elixir-ls/releases/download/v0.5.0/elixir-ls.zip
@ -57,12 +57,13 @@ mv rebar3 /usr/bin/rebar3
# Go # Go
export GO111MODULE=on export GO111MODULE=on
export GOPATH=/tmp/go export GOPATH="$PWD/go"
mv /tmp/go/bin/gopls /usr/bin/gopls go get golang.org/x/tools/gopls@latest
rm -rf /tmp/go mv go/bin/gopls /usr/bin/gopls
rm -rf go
# Haskell # Haskell
wget https://get.haskellstack.org/stable/linux-x86_64-static.tar.gz wget -nv https://get.haskellstack.org/stable/linux-x86_64-static.tar.gz
tar -xf linux-x86_64-static.tar.gz tar -xf linux-x86_64-static.tar.gz
mv stack-*-linux-x86_64-static/stack /usr/bin/stack mv stack-*-linux-x86_64-static/stack /usr/bin/stack
rm -rf stack-*-linux-x86_64-static linux-x86_64-static.tar.gz rm -rf stack-*-linux-x86_64-static linux-x86_64-static.tar.gz

424
scripts/my_init Executable file
View File

@ -0,0 +1,424 @@
#!/usr/bin/python3 -u
# -*- coding: utf-8 -*-
#
# From https://github.com/phusion/baseimage-docker/blob/5078b027ba58cce8887acb5c1add0bb8d56f5d38/image/bin/my_init
# Copyright 2013-2015 Phusion Holding B.V. under MIT License
# See https://github.com/phusion/baseimage-docker/blob/5078b027ba58cce8887acb5c1add0bb8d56f5d38/LICENSE.txt
import argparse
import errno
import json
import os
import os.path
import re
import signal
import stat
import sys
import time
ENV_INIT_DIRECTORY = os.environ.get('ENV_INIT_DIRECTORY', '/etc/my_init.d')
KILL_PROCESS_TIMEOUT = int(os.environ.get('KILL_PROCESS_TIMEOUT', 30))
KILL_ALL_PROCESSES_TIMEOUT = int(os.environ.get('KILL_ALL_PROCESSES_TIMEOUT', 30))
LOG_LEVEL_ERROR = 1
LOG_LEVEL_WARN = 1
LOG_LEVEL_INFO = 2
LOG_LEVEL_DEBUG = 3
SHENV_NAME_WHITELIST_REGEX = re.compile('\W')
log_level = None
terminated_child_processes = {}
_find_unsafe = re.compile(r'[^\w@%+=:,./-]').search
class AlarmException(Exception):
pass
def error(message):
if log_level >= LOG_LEVEL_ERROR:
sys.stderr.write("*** %s\n" % message)
def warn(message):
if log_level >= LOG_LEVEL_WARN:
sys.stderr.write("*** %s\n" % message)
def info(message):
if log_level >= LOG_LEVEL_INFO:
sys.stderr.write("*** %s\n" % message)
def debug(message):
if log_level >= LOG_LEVEL_DEBUG:
sys.stderr.write("*** %s\n" % message)
def ignore_signals_and_raise_keyboard_interrupt(signame):
signal.signal(signal.SIGTERM, signal.SIG_IGN)
signal.signal(signal.SIGINT, signal.SIG_IGN)
raise KeyboardInterrupt(signame)
def raise_alarm_exception():
raise AlarmException('Alarm')
def listdir(path):
try:
result = os.stat(path)
except OSError:
return []
if stat.S_ISDIR(result.st_mode):
return sorted(os.listdir(path))
else:
return []
def is_exe(path):
try:
return os.path.isfile(path) and os.access(path, os.X_OK)
except OSError:
return False
def import_envvars(clear_existing_environment=True, override_existing_environment=True):
if not os.path.exists("/etc/container_environment"):
return
new_env = {}
for envfile in listdir("/etc/container_environment"):
name = os.path.basename(envfile)
with open("/etc/container_environment/" + envfile, "r") as f:
# Text files often end with a trailing newline, which we
# don't want to include in the env variable value. See
# https://github.com/phusion/baseimage-docker/pull/49
value = re.sub('\n\Z', '', f.read())
new_env[name] = value
if clear_existing_environment:
os.environ.clear()
for name, value in new_env.items():
if override_existing_environment or name not in os.environ:
os.environ[name] = value
def export_envvars(to_dir=True):
if not os.path.exists("/etc/container_environment"):
return
shell_dump = ""
for name, value in os.environ.items():
if name in ['HOME', 'USER', 'GROUP', 'UID', 'GID', 'SHELL']:
continue
if to_dir:
with open("/etc/container_environment/" + name, "w") as f:
f.write(value)
shell_dump += "export " + sanitize_shenvname(name) + "=" + shquote(value) + "\n"
with open("/etc/container_environment.sh", "w") as f:
f.write(shell_dump)
with open("/etc/container_environment.json", "w") as f:
f.write(json.dumps(dict(os.environ)))
def shquote(s):
"""Return a shell-escaped version of the string *s*."""
if not s:
return "''"
if _find_unsafe(s) is None:
return s
# use single quotes, and put single quotes into double quotes
# the string $'b is then quoted as '$'"'"'b'
return "'" + s.replace("'", "'\"'\"'") + "'"
def sanitize_shenvname(s):
"""Return string with [0-9a-zA-Z_] characters"""
return re.sub(SHENV_NAME_WHITELIST_REGEX, "_", s)
# Waits for the child process with the given PID, while at the same time
# reaping any other child processes that have exited (e.g. adopted child
# processes that have terminated).
def waitpid_reap_other_children(pid):
global terminated_child_processes
status = terminated_child_processes.get(pid)
if status:
# A previous call to waitpid_reap_other_children(),
# with an argument not equal to the current argument,
# already waited for this process. Return the status
# that was obtained back then.
del terminated_child_processes[pid]
return status
done = False
status = None
while not done:
try:
# https://github.com/phusion/baseimage-docker/issues/151#issuecomment-92660569
this_pid, status = os.waitpid(pid, os.WNOHANG)
if this_pid == 0:
this_pid, status = os.waitpid(-1, 0)
if this_pid == pid:
done = True
else:
# Save status for later.
terminated_child_processes[this_pid] = status
except OSError as e:
if e.errno == errno.ECHILD or e.errno == errno.ESRCH:
return None
else:
raise
return status
def stop_child_process(name, pid, signo=signal.SIGTERM, time_limit=KILL_PROCESS_TIMEOUT):
info("Shutting down %s (PID %d)..." % (name, pid))
try:
os.kill(pid, signo)
except OSError:
pass
signal.alarm(time_limit)
try:
try:
waitpid_reap_other_children(pid)
except OSError:
pass
except AlarmException:
warn("%s (PID %d) did not shut down in time. Forcing it to exit." % (name, pid))
try:
os.kill(pid, signal.SIGKILL)
except OSError:
pass
try:
waitpid_reap_other_children(pid)
except OSError:
pass
finally:
signal.alarm(0)
def run_command_killable(*argv):
filename = argv[0]
status = None
pid = os.spawnvp(os.P_NOWAIT, filename, argv)
try:
status = waitpid_reap_other_children(pid)
except BaseException:
warn("An error occurred. Aborting.")
stop_child_process(filename, pid)
raise
if status != 0:
if status is None:
error("%s exited with unknown status\n" % filename)
else:
error("%s failed with status %d\n" % (filename, os.WEXITSTATUS(status)))
sys.exit(1)
def run_command_killable_and_import_envvars(*argv):
run_command_killable(*argv)
import_envvars()
export_envvars(False)
def kill_all_processes(time_limit):
info("Killing all processes...")
try:
os.kill(-1, signal.SIGTERM)
except OSError:
pass
signal.alarm(time_limit)
try:
# Wait until no more child processes exist.
done = False
while not done:
try:
os.waitpid(-1, 0)
except OSError as e:
if e.errno == errno.ECHILD:
done = True
else:
raise
except AlarmException:
warn("Not all processes have exited in time. Forcing them to exit.")
try:
os.kill(-1, signal.SIGKILL)
except OSError:
pass
finally:
signal.alarm(0)
def run_startup_files():
# Run ENV_INIT_DIRECTORY/*
for name in listdir(ENV_INIT_DIRECTORY):
filename = os.path.join(ENV_INIT_DIRECTORY, name)
if is_exe(filename):
info("Running %s..." % filename)
run_command_killable_and_import_envvars(filename)
# Run /etc/rc.local.
if is_exe("/etc/rc.local"):
info("Running /etc/rc.local...")
run_command_killable_and_import_envvars("/etc/rc.local")
def run_pre_shutdown_scripts():
debug("Running pre-shutdown scripts...")
# Run /etc/my_init.pre_shutdown.d/*
for name in listdir("/etc/my_init.pre_shutdown.d"):
filename = "/etc/my_init.pre_shutdown.d/" + name
if is_exe(filename):
info("Running %s..." % filename)
run_command_killable(filename)
def run_post_shutdown_scripts():
debug("Running post-shutdown scripts...")
# Run /etc/my_init.post_shutdown.d/*
for name in listdir("/etc/my_init.post_shutdown.d"):
filename = "/etc/my_init.post_shutdown.d/" + name
if is_exe(filename):
info("Running %s..." % filename)
run_command_killable(filename)
def start_runit():
info("Booting runit daemon...")
pid = os.spawnl(os.P_NOWAIT, "/usr/bin/runsvdir", "/usr/bin/runsvdir",
"-P", "/etc/service")
info("Runit started as PID %d" % pid)
return pid
def wait_for_runit_or_interrupt(pid):
status = waitpid_reap_other_children(pid)
return (True, status)
def shutdown_runit_services(quiet=False):
if not quiet:
debug("Begin shutting down runit services...")
os.system("/usr/bin/sv -w %d force-stop /etc/service/* > /dev/null" % KILL_PROCESS_TIMEOUT)
def wait_for_runit_services():
debug("Waiting for runit services to exit...")
done = False
while not done:
done = os.system("/usr/bin/sv status /etc/service/* | grep -q '^run:'") != 0
if not done:
time.sleep(0.1)
# According to https://github.com/phusion/baseimage-docker/issues/315
# there is a bug or race condition in Runit, causing it
# not to shutdown services that are already being started.
# So during shutdown we repeatedly instruct Runit to shutdown
# services.
shutdown_runit_services(True)
def install_insecure_key():
info("Installing insecure SSH key for user root")
run_command_killable("/usr/sbin/enable_insecure_key")
def main(args):
import_envvars(False, False)
export_envvars()
if args.enable_insecure_key:
install_insecure_key()
if not args.skip_startup_files:
run_startup_files()
runit_exited = False
exit_code = None
if not args.skip_runit:
runit_pid = start_runit()
try:
exit_status = None
if len(args.main_command) == 0:
runit_exited, exit_code = wait_for_runit_or_interrupt(runit_pid)
if runit_exited:
if exit_code is None:
info("Runit exited with unknown status")
exit_status = 1
else:
exit_status = os.WEXITSTATUS(exit_code)
info("Runit exited with status %d" % exit_status)
else:
info("Running %s..." % " ".join(args.main_command))
pid = os.spawnvp(os.P_NOWAIT, args.main_command[0], args.main_command)
try:
exit_code = waitpid_reap_other_children(pid)
if exit_code is None:
info("%s exited with unknown status." % args.main_command[0])
exit_status = 1
else:
exit_status = os.WEXITSTATUS(exit_code)
info("%s exited with status %d." % (args.main_command[0], exit_status))
except KeyboardInterrupt:
stop_child_process(args.main_command[0], pid)
raise
except BaseException:
warn("An error occurred. Aborting.")
stop_child_process(args.main_command[0], pid)
raise
sys.exit(exit_status)
finally:
if not args.skip_runit:
run_pre_shutdown_scripts()
shutdown_runit_services()
if not runit_exited:
stop_child_process("runit daemon", runit_pid)
wait_for_runit_services()
run_post_shutdown_scripts()
# Parse options.
parser = argparse.ArgumentParser(description='Initialize the system.')
parser.add_argument('main_command', metavar='MAIN_COMMAND', type=str, nargs='*',
help='The main command to run. (default: runit)')
parser.add_argument('--enable-insecure-key', dest='enable_insecure_key',
action='store_const', const=True, default=False,
help='Install the insecure SSH key')
parser.add_argument('--skip-startup-files', dest='skip_startup_files',
action='store_const', const=True, default=False,
help='Skip running /etc/my_init.d/* and /etc/rc.local')
parser.add_argument('--skip-runit', dest='skip_runit',
action='store_const', const=True, default=False,
help='Do not run runit services')
parser.add_argument('--no-kill-all-on-exit', dest='kill_all_on_exit',
action='store_const', const=False, default=True,
help='Don\'t kill all processes on the system upon exiting')
parser.add_argument('--quiet', dest='log_level',
action='store_const', const=LOG_LEVEL_WARN, default=LOG_LEVEL_INFO,
help='Only print warnings and errors')
args = parser.parse_args()
log_level = args.log_level
if args.skip_runit and len(args.main_command) == 0:
error("When --skip-runit is given, you must also pass a main command.")
sys.exit(1)
# Run main function.
signal.signal(signal.SIGTERM, lambda signum, frame: ignore_signals_and_raise_keyboard_interrupt('SIGTERM'))
signal.signal(signal.SIGINT, lambda signum, frame: ignore_signals_and_raise_keyboard_interrupt('SIGINT'))
signal.signal(signal.SIGALRM, lambda signum, frame: raise_alarm_exception())
try:
main(args)
except KeyboardInterrupt:
warn("Init system aborted.")
exit(2)
finally:
if args.kill_all_on_exit:
kill_all_processes(KILL_ALL_PROCESSES_TIMEOUT)

View File

@ -5,6 +5,6 @@ set -o pipefail
mkdir -p /tmp/riju mkdir -p /tmp/riju
if [[ -x system/out/riju-system-privileged ]]; then if [[ -x system/out/riju-system-privileged ]]; then
system/out/riju-system-privileged teardown "*" || true system/out/riju-system-privileged teardown "*" "*" || true
fi fi
chmod a=x,u=rwx /tmp/riju chmod a=x,u=rwx /tmp/riju

View File

@ -14,7 +14,7 @@ const int MAX_UID = 65000;
int privileged; int privileged;
void die(char *msg) void __attribute__ ((noreturn)) die(char *msg)
{ {
fprintf(stderr, "%s\n", msg); fprintf(stderr, "%s\n", msg);
exit(1); exit(1);
@ -24,8 +24,8 @@ void die_with_usage()
{ {
die("usage:\n" die("usage:\n"
" riju-system-privileged useradd UID\n" " riju-system-privileged useradd UID\n"
" riju-system-privileged spawn UID CMDLINE...\n"
" riju-system-privileged setup UID UUID\n" " riju-system-privileged setup UID UUID\n"
" riju-system-privileged spawn UID UUID CMDLINE...\n"
" riju-system-privileged teardown UUID"); " riju-system-privileged teardown UUID");
} }
@ -60,12 +60,12 @@ void useradd(int uid)
if (asprintf(&cmdline, "groupadd -g %1$d riju%1$d", uid) < 0) if (asprintf(&cmdline, "groupadd -g %1$d riju%1$d", uid) < 0)
die("asprintf failed"); die("asprintf failed");
int status = system(cmdline); int status = system(cmdline);
if (status) if (status != 0)
die("groupadd failed"); die("groupadd failed");
if (asprintf(&cmdline, "useradd -M -N -l -r -u %1$d -g %1$d -p '!' -s /usr/bin/bash riju%1$d", uid) < 0) if (asprintf(&cmdline, "useradd -M -N -l -r -u %1$d -g %1$d -p '!' -s /usr/bin/bash riju%1$d", uid) < 0)
die("asprintf failed"); die("asprintf failed");
status = system(cmdline); status = system(cmdline);
if (status) if (status != 0)
die("useradd failed"); die("useradd failed");
} }
@ -97,17 +97,44 @@ void setup(int uid, char *uuid)
: "install -d -m 700 /tmp/riju/%2$s", uid, uuid) < 0) : "install -d -m 700 /tmp/riju/%2$s", uid, uuid) < 0)
die("asprintf failed"); die("asprintf failed");
int status = system(cmdline); int status = system(cmdline);
if (status) if (status != 0)
die("install failed"); die("install failed");
} }
void teardown(char *uuid) void teardown(int uid, char *uuid)
{ {
char *cmdline; char *cmdline;
int status;
char *users;
if (uid >= MIN_UID && uid < MAX_UID) {
if (asprintf(&users, "%d", uid) < 0)
die("asprintf failed");
} else {
cmdline = "getent passwd | grep -Eo '^riju[0-9]{4}' | paste -s -d, - | tr -d '\n'";
FILE *fp = popen(cmdline, "r");
if (fp == NULL)
die("popen failed");
static char buf[(MAX_UID - MIN_UID) * 9];
if (fgets(buf, sizeof(buf), fp) == NULL) {
if (feof(fp))
users = NULL;
else {
die("fgets failed");
}
} else
users = buf;
}
if (users != NULL) {
if (asprintf(&cmdline, "pkill -SIGKILL --uid %s", users) < 0)
die("asprintf failed");
status = system(cmdline);
if (status != 0 && status != 256)
die("pkill failed");
}
if (asprintf(&cmdline, "rm -rf /tmp/riju/%s", uuid) < 0) if (asprintf(&cmdline, "rm -rf /tmp/riju/%s", uuid) < 0)
die("asprintf failed"); die("asprintf failed");
int status = system(cmdline); status = system(cmdline);
if (status) if (status != 0)
die("rm failed"); die("rm failed");
} }
@ -140,10 +167,11 @@ int main(int argc, char **argv)
return 0; return 0;
} }
if (!strcmp(argv[1], "teardown")) { if (!strcmp(argv[1], "teardown")) {
if (argc != 3) if (argc != 4)
die_with_usage(); die_with_usage();
char *uuid = strcmp(argv[2], "*") ? parseUUID(argv[2]) : "*"; int uid = strcmp(argv[2], "*") ? parseUID(argv[2]) : -1;
teardown(uuid); char *uuid = strcmp(argv[3], "*") ? parseUUID(argv[3]) : "*";
teardown(uid, uuid);
return 0; return 0;
} }
die_with_usage(); die_with_usage();

View File

@ -1,7 +1,8 @@
{ {
"compilerOptions": { "compilerOptions": {
"outDir": "./frontend/out", "outDir": "./frontend/out",
"rootDir": "./frontend/src" "rootDir": "./frontend/src",
"target": "ES3"
}, },
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"include": ["frontend/src"] "include": ["frontend/src"]

View File

@ -4,7 +4,8 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"rootDir": "./backend/src", "rootDir": "./backend/src",
"sourceMap": true, "sourceMap": true,
"strict": true "strict": true,
"target": "ES5"
}, },
"include": ["backend/src"] "include": ["backend/src"]
} }

View File

@ -882,6 +882,11 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/node-cleanup@^2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@types/node-cleanup/-/node-cleanup-2.1.1.tgz#c8f78a648897d2a40ed10632268ce15d343cc191"
integrity sha512-Q1s5Sszz6YfhaGr1pbaZihr9IYaiQT0aOK/3c2qb9lOUbEBhcAb9ZEU7RBTtopnHSIJF80adLRcOGTay2W5QVQ==
"@types/node@*": "@types/node@*":
version "14.0.11" version "14.0.11"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.11.tgz#61d4886e2424da73b7b25547f59fdcb534c165a3" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.11.tgz#61d4886e2424da73b7b25547f59fdcb534c165a3"
@ -3450,11 +3455,6 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
node-cleanup@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/node-cleanup/-/node-cleanup-2.1.2.tgz#7ac19abd297e09a7f72a71545d951b517e4dde2c"
integrity sha1-esGavSl+Caf3KnFUXZUbUX5N3iw=
node-libs-browser@^2.2.1: node-libs-browser@^2.2.1:
version "2.2.1" version "2.2.1"
resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425"