Refactor api.ts, plug resource leaks, fix build
This commit is contained in:
parent
bb14a6c9ab
commit
0b8d5d244d
|
@ -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"]
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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}`);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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}`)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue