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
|
||||
|
||||
COPY scripts/my_init /usr/bin/my_init
|
||||
|
||||
COPY scripts/docker-install-phase0.bash /tmp/
|
||||
RUN /tmp/docker-install-phase0.bash
|
||||
|
||||
|
@ -44,6 +46,6 @@ RUN chmod go-rwx /home/docker
|
|||
EXPOSE 6119
|
||||
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/
|
||||
CMD ["bash"]
|
||||
|
|
|
@ -4,6 +4,8 @@ FROM ubuntu:focal
|
|||
# prod, it's not actually read by anything.
|
||||
ARG UID
|
||||
|
||||
COPY scripts/my_init /usr/bin/my_init
|
||||
|
||||
COPY scripts/docker-install-phase0.bash /tmp/
|
||||
RUN /tmp/docker-install-phase0.bash
|
||||
|
||||
|
@ -46,7 +48,7 @@ RUN chmod go-rwx /home/docker
|
|||
EXPOSE 6119
|
||||
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/
|
||||
CMD ["yarn", "run", "server"]
|
||||
|
||||
|
|
|
@ -43,8 +43,9 @@ container first:
|
|||
|
||||
$ make docker
|
||||
|
||||
Note that building the image takes about 30 minutes on high-end
|
||||
hardware and ethernet, and it requires about 15 GB of disk space.
|
||||
Note that building the image can take up to 45 minutes even on
|
||||
high-end hardware and internet, and it requires about 15 GB of disk
|
||||
space.
|
||||
|
||||
## Flag
|
||||
|
||||
|
|
|
@ -9,288 +9,354 @@ import { v4 as getUUID } from "uuid";
|
|||
|
||||
import { LangConfig, langs } from "./langs";
|
||||
import { borrowUser } from "./users";
|
||||
import {
|
||||
callPrivileged,
|
||||
getEnv,
|
||||
rijuSystemPrivileged,
|
||||
spawnPrivileged,
|
||||
} from "./util";
|
||||
import * as util from "./util";
|
||||
import { Context, Options, bash } from "./util";
|
||||
|
||||
const allSessions: Set<Session> = new Set();
|
||||
|
||||
export class Session {
|
||||
ws: WebSocket;
|
||||
uuid: string;
|
||||
code: string | null;
|
||||
config: LangConfig;
|
||||
term: { pty: IPty | null; live: boolean };
|
||||
lang: string;
|
||||
|
||||
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: {
|
||||
proc: ChildProcess;
|
||||
reader: rpc.StreamMessageReader;
|
||||
writer: rpc.StreamMessageWriter;
|
||||
} | null;
|
||||
daemon: ChildProcess | null;
|
||||
ws: WebSocket;
|
||||
homedir: string | null;
|
||||
uid: number | null;
|
||||
uidCleanup: (() => Promise<void>) | null;
|
||||
} | null = null;
|
||||
daemon: { proc: ChildProcess } | null = null;
|
||||
|
||||
get homedir() {
|
||||
return `/tmp/riju/${this.uuid}`;
|
||||
}
|
||||
|
||||
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}`);
|
||||
|
||||
constructor(ws: WebSocket, lang: string) {
|
||||
this.uuid = getUUID();
|
||||
this.log(`Creating session, language ${lang}`);
|
||||
this.ws = ws;
|
||||
this.config = langs[lang];
|
||||
this.term = { pty: null, live: false };
|
||||
this.lsp = null;
|
||||
this.daemon = null;
|
||||
this.code = null;
|
||||
this.homedir = null;
|
||||
this.uid = null;
|
||||
this.uidCleanup = null;
|
||||
ws.on("message", this.handleClientMessage);
|
||||
ws.on("close", () =>
|
||||
this.cleanup().catch((err) => {
|
||||
this.log(`Error during session cleanup`);
|
||||
console.log(err);
|
||||
})
|
||||
);
|
||||
this.run().catch((err) => {
|
||||
this.log(`Error while setting up environment for pty`);
|
||||
console.log(err);
|
||||
this.send({ event: "terminalClear" });
|
||||
this.send({
|
||||
event: "terminalOutput",
|
||||
output: `Riju encountered an unexpected error: ${err}
|
||||
\rYou may want to save your code and refresh the page.
|
||||
`,
|
||||
});
|
||||
});
|
||||
this.uuid = getUUID();
|
||||
this.lang = lang;
|
||||
this.log(`Creating session, language ${this.lang}`);
|
||||
this.setup();
|
||||
}
|
||||
send = (msg: any) => {
|
||||
|
||||
run = async (args: string[], options?: Options) => {
|
||||
return await util.run(args, this.log, options);
|
||||
};
|
||||
|
||||
privilegedSetup = () => util.privilegedSetup(this.context);
|
||||
privilegedSpawn = (args: string[]) =>
|
||||
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"),
|
||||
})
|
||||
);
|
||||
daemonProc.on("exit", (code, signal) =>
|
||||
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();
|
||||
}
|
||||
};
|
||||
handleClientMessage = (event: string) => {
|
||||
let msg: any;
|
||||
try {
|
||||
msg = JSON.parse(event);
|
||||
} catch (err) {
|
||||
this.log(`Failed to parse client message: ${msg}`);
|
||||
return;
|
||||
}
|
||||
switch (msg?.event) {
|
||||
case "terminalInput":
|
||||
if (!this.term) {
|
||||
this.log(`Got terminal input before pty was started`);
|
||||
} else if (typeof msg.input !== "string") {
|
||||
this.log(`Got malformed terminal input message`);
|
||||
} else {
|
||||
this.term.pty!.write(msg.input);
|
||||
}
|
||||
break;
|
||||
case "runCode":
|
||||
if (typeof msg.code !== "string") {
|
||||
this.log(`Got malformed run message`);
|
||||
} else {
|
||||
this.code = msg.code;
|
||||
this.run();
|
||||
}
|
||||
break;
|
||||
case "lspInput":
|
||||
if (!this.lsp) {
|
||||
this.log(`Got LSP input before language server was started`);
|
||||
} else {
|
||||
this.lsp.writer.write(msg.input);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
this.log(`Got unknown message type: ${msg.event}`);
|
||||
break;
|
||||
}
|
||||
};
|
||||
run = async () => {
|
||||
if (this.uid === null) {
|
||||
({ uid: this.uid, cleanup: this.uidCleanup } = await borrowUser(
|
||||
this.log
|
||||
));
|
||||
this.log(`Borrowed uid ${this.uid}`);
|
||||
}
|
||||
const {
|
||||
name,
|
||||
daemon,
|
||||
repl,
|
||||
main,
|
||||
suffix,
|
||||
createEmpty,
|
||||
compile,
|
||||
run,
|
||||
lspSetup,
|
||||
lsp,
|
||||
template,
|
||||
hacks,
|
||||
} = this.config;
|
||||
if (this.term.pty) {
|
||||
this.term.pty.kill();
|
||||
this.term.live = false;
|
||||
}
|
||||
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;
|
||||
if (!run) {
|
||||
cmdline = `echo 'Support for ${this.config.name} is not yet implemented.'`;
|
||||
} else if (this.code) {
|
||||
cmdline = run;
|
||||
if (compile) {
|
||||
cmdline = `( ${compile} ) && ( ${run} )`;
|
||||
}
|
||||
} else if (repl) {
|
||||
cmdline = repl;
|
||||
} else {
|
||||
cmdline = `echo '${name} has no REPL, press Run to see it in action'`;
|
||||
}
|
||||
let code = this.code;
|
||||
if (this.code === null) {
|
||||
code = createEmpty ? "" : template;
|
||||
}
|
||||
if (code && suffix) {
|
||||
code += suffix;
|
||||
}
|
||||
if (main.includes("/")) {
|
||||
await spawnPrivileged(
|
||||
this.uid,
|
||||
this.uuid,
|
||||
["mkdir", "-p", path.dirname(path.resolve(this.homedir, main))],
|
||||
this.log
|
||||
);
|
||||
}
|
||||
await spawnPrivileged(
|
||||
this.uid,
|
||||
this.uuid,
|
||||
["sh", "-c", `cat > ${path.resolve(this.homedir, main)}`],
|
||||
this.log,
|
||||
{ input: code as string }
|
||||
);
|
||||
if (hacks && hacks.includes("ghci-config") && run) {
|
||||
if (this.code) {
|
||||
const contents = ":load Main\nmain\n";
|
||||
await spawnPrivileged(
|
||||
this.uid,
|
||||
this.uuid,
|
||||
["sh", "-c", `cat > ${path.resolve(this.homedir, ".ghci")}`],
|
||||
this.log,
|
||||
{ input: contents }
|
||||
);
|
||||
} else {
|
||||
await spawnPrivileged(
|
||||
this.uid,
|
||||
this.uuid,
|
||||
["rm", "-f", path.resolve(this.homedir, ".ghci")],
|
||||
this.log
|
||||
);
|
||||
}
|
||||
}
|
||||
const args = [
|
||||
rijuSystemPrivileged,
|
||||
"spawn",
|
||||
`${this.uid}`,
|
||||
`${this.uuid}`,
|
||||
"bash",
|
||||
"-c",
|
||||
cmdline,
|
||||
];
|
||||
const env = getEnv(this.uuid);
|
||||
const term = {
|
||||
pty: pty.spawn(args[0], args.slice(1), {
|
||||
name: "xterm-color",
|
||||
env,
|
||||
}),
|
||||
live: true,
|
||||
};
|
||||
this.term = term;
|
||||
term.pty.on("data", (data) => {
|
||||
// Capture term in closure so that we don't keep sending output
|
||||
// from the old pty even after it's been killed (see ghci).
|
||||
if (term.live) {
|
||||
this.send({ event: "terminalOutput", output: data });
|
||||
}
|
||||
|
||||
sendError = async (err: any) => {
|
||||
await this.send({ event: "terminalClear" });
|
||||
await this.send({
|
||||
event: "terminalOutput",
|
||||
output: `Riju encountered an unexpected error: ${err}
|
||||
\r
|
||||
\rYou may want to save your code and refresh the page.
|
||||
`,
|
||||
});
|
||||
if (daemon && this.daemon === null) {
|
||||
this.daemon = spawn("bash", ["-c", daemon], { env: getEnv(this.uuid) });
|
||||
this.daemon.on("exit", (code) =>
|
||||
this.send({ event: "daemonCrashed", code })
|
||||
);
|
||||
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
|
||||
);
|
||||
};
|
||||
|
||||
logBadMessage = (msg: any) => {
|
||||
this.log(`Got malformed message from client: ${msg}`);
|
||||
};
|
||||
|
||||
receive = async (event: string) => {
|
||||
try {
|
||||
if (this.tearingDown) {
|
||||
return;
|
||||
}
|
||||
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}` });
|
||||
let msg: any;
|
||||
try {
|
||||
msg = JSON.parse(event);
|
||||
} catch (err) {
|
||||
this.log(`Failed to parse message from client: ${msg}`);
|
||||
return;
|
||||
}
|
||||
switch (msg && msg.event) {
|
||||
case "terminalInput":
|
||||
if (typeof msg.input !== "string") {
|
||||
this.logBadMessage(msg);
|
||||
break;
|
||||
}
|
||||
if (!this.term) {
|
||||
this.log("terminalInput ignored because term is null");
|
||||
break;
|
||||
}
|
||||
this.term!.pty.write(msg.input);
|
||||
break;
|
||||
case "runCode":
|
||||
if (typeof msg.code !== "string") {
|
||||
this.logBadMessage(msg);
|
||||
break;
|
||||
}
|
||||
await this.runCode(msg.code);
|
||||
break;
|
||||
case "lspInput":
|
||||
if (typeof msg.input !== "object" || !msg) {
|
||||
this.logBadMessage(msg);
|
||||
break;
|
||||
}
|
||||
if (!this.lsp) {
|
||||
this.log(`lspInput ignored because lsp is null`);
|
||||
break;
|
||||
}
|
||||
this.lsp.writer.write(msg.input);
|
||||
break;
|
||||
default:
|
||||
this.logBadMessage(msg);
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
this.log(`Error while handling message from client`);
|
||||
console.log(err);
|
||||
this.sendError(err);
|
||||
}
|
||||
};
|
||||
cleanup = async () => {
|
||||
this.log(`Cleaning up session`);
|
||||
if (this.term.pty) {
|
||||
await spawnPrivileged(
|
||||
this.uid!,
|
||||
this.uuid,
|
||||
["bash", "-c", `kill -9 ${this.term.pty.pid} 2>/dev/null || true`],
|
||||
this.log
|
||||
|
||||
runCode = async (code?: string) => {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
repl,
|
||||
main,
|
||||
suffix,
|
||||
createEmpty,
|
||||
compile,
|
||||
run,
|
||||
template,
|
||||
hacks,
|
||||
} = this.config;
|
||||
if (this.term) {
|
||||
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 = null;
|
||||
}
|
||||
this.send({ event: "terminalClear" });
|
||||
let cmdline: string;
|
||||
if (code) {
|
||||
cmdline = run;
|
||||
if (compile) {
|
||||
cmdline = `( ${compile} ) && ( ${run} )`;
|
||||
}
|
||||
} else if (repl) {
|
||||
cmdline = repl;
|
||||
} else {
|
||||
cmdline = `echo '${name} has no REPL, press Run to see it in action'`;
|
||||
}
|
||||
if (code === undefined) {
|
||||
code = createEmpty ? "" : template;
|
||||
}
|
||||
if (code && suffix) {
|
||||
code += suffix;
|
||||
}
|
||||
if (main.includes("/")) {
|
||||
await this.run(
|
||||
this.privilegedSpawn([
|
||||
"mkdir",
|
||||
"-p",
|
||||
path.dirname(`${this.homedir}/${main}`),
|
||||
])
|
||||
);
|
||||
}
|
||||
await this.run(
|
||||
this.privilegedSpawn([
|
||||
"sh",
|
||||
"-c",
|
||||
`cat > ${path.resolve(this.homedir, main)}`,
|
||||
]),
|
||||
{ input: code }
|
||||
);
|
||||
if (hacks && hacks.includes("ghci-config") && run) {
|
||||
if (code) {
|
||||
await this.run(
|
||||
this.privilegedSpawn(["sh", "-c", `cat > ${this.homedir}/.ghci`]),
|
||||
{ input: ":load Main\nmain\n" }
|
||||
);
|
||||
} else {
|
||||
await this.run(
|
||||
this.privilegedSpawn(["rm", "-f", `${this.homedir}/.ghci`])
|
||||
);
|
||||
}
|
||||
}
|
||||
const termArgs = this.privilegedSpawn(bash(cmdline));
|
||||
const term = {
|
||||
pty: pty.spawn(termArgs[0], termArgs.slice(1), {
|
||||
name: "xterm-color",
|
||||
env: this.env,
|
||||
}),
|
||||
live: true,
|
||||
};
|
||||
this.term = term;
|
||||
this.term.pty.on("data", (data) => {
|
||||
// Capture term in closure so that we don't keep sending output
|
||||
// from the old pty even after it's been killed (see ghci).
|
||||
if (term.live) {
|
||||
this.send({ event: "terminalOutput", output: data });
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
this.log(`Error while running user code`);
|
||||
console.log(err);
|
||||
this.sendError(err);
|
||||
}
|
||||
if (this.lsp !== null) {
|
||||
await spawnPrivileged(
|
||||
this.uid!,
|
||||
this.uuid,
|
||||
["bash", "-c", `kill -9 ${this.lsp.proc.pid} 2>/dev/null || true`],
|
||||
this.log
|
||||
);
|
||||
}
|
||||
if (this.homedir) {
|
||||
await callPrivileged(["teardown", this.uuid], this.log);
|
||||
}
|
||||
if (this.uidCleanup) {
|
||||
await this.uidCleanup();
|
||||
this.log(`Returned uid ${this.uid}`);
|
||||
};
|
||||
|
||||
teardown = async () => {
|
||||
try {
|
||||
if (this.tearingDown) {
|
||||
return;
|
||||
}
|
||||
this.log(`Tearing down session`);
|
||||
this.tearingDown = true;
|
||||
allSessions.delete(this);
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
await this.run(this.privilegedTeardown());
|
||||
await this.returnUID();
|
||||
this.ws.terminate();
|
||||
} catch (err) {
|
||||
this.log(`Error during teardown`);
|
||||
console.log(err);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,7 +5,13 @@ import { v4 as getUUID } from "uuid";
|
|||
|
||||
import { langs } from "./langs";
|
||||
import { borrowUser } from "./users";
|
||||
import { callPrivileged, getEnv, rijuSystemPrivileged } from "./util";
|
||||
import {
|
||||
getEnv,
|
||||
privilegedSetup,
|
||||
privilegedSpawn,
|
||||
privilegedTeardown,
|
||||
run,
|
||||
} from "./util";
|
||||
|
||||
function die(msg: any) {
|
||||
console.error(msg);
|
||||
|
@ -18,9 +24,9 @@ function log(msg: any) {
|
|||
|
||||
async function main() {
|
||||
const uuid = getUUID();
|
||||
const { uid, cleanup } = await borrowUser(log);
|
||||
await callPrivileged(["setup", `${uid}`, uuid], log);
|
||||
const args = [rijuSystemPrivileged, "spawn", `${uid}`, `${uuid}`, "bash"];
|
||||
const { uid, returnUID } = await borrowUser(log);
|
||||
await run(privilegedSetup({ uid, uuid }), log);
|
||||
const args = privilegedSpawn({ uid, uuid }, ["bash"]);
|
||||
const proc = spawn(args[0], args.slice(1), {
|
||||
env: getEnv(uuid),
|
||||
stdio: "inherit",
|
||||
|
@ -29,7 +35,8 @@ async function main() {
|
|||
proc.on("error", reject);
|
||||
proc.on("exit", resolve);
|
||||
});
|
||||
await cleanup();
|
||||
await run(privilegedTeardown({ uid, uuid }), log);
|
||||
await returnUID();
|
||||
}
|
||||
|
||||
main().catch(die);
|
||||
|
|
|
@ -100,7 +100,7 @@ if (useTLS) {
|
|||
httpsServer.listen(tlsPort, host, () =>
|
||||
console.log(`Listening on https://${host}:${tlsPort}`)
|
||||
);
|
||||
http
|
||||
const server = http
|
||||
.createServer((req, res) => {
|
||||
res.writeHead(301, {
|
||||
Location: "https://" + req.headers["host"] + req.url,
|
||||
|
@ -112,7 +112,7 @@ if (useTLS) {
|
|||
);
|
||||
} else {
|
||||
addWebsocket(app, undefined);
|
||||
app.listen(port, host, () =>
|
||||
const server = app.listen(port, host, () =>
|
||||
console.log(`Listening on http://${host}:${port}`)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import * as _ from "lodash";
|
|||
import * as parsePasswd from "parse-passwd";
|
||||
|
||||
import { PRIVILEGED } from "./config";
|
||||
import { callPrivileged } from "./util";
|
||||
import { privilegedUseradd, run } from "./util";
|
||||
|
||||
// Keep in sync with system/src/riju-system-privileged.c
|
||||
const MIN_UID = 2000;
|
||||
|
@ -21,7 +21,7 @@ let lock = new AsyncLock();
|
|||
|
||||
async function readExistingUsers(log: (msg: string) => void) {
|
||||
availIds = parsePasswd(
|
||||
await new Promise((resolve, reject) =>
|
||||
await new Promise((resolve: (result: string) => void, reject) =>
|
||||
fs.readFile("/etc/passwd", "utf-8", (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
|
@ -43,7 +43,7 @@ async function createUser(log: (msg: string) => void): Promise<number> {
|
|||
throw new Error("too many users");
|
||||
}
|
||||
const uid = nextId!;
|
||||
await callPrivileged(["useradd", `${uid}`], log);
|
||||
await run(privilegedUseradd(uid), log);
|
||||
log(`Created new user with ID ${uid}`);
|
||||
nextId! += 1;
|
||||
return uid;
|
||||
|
@ -51,7 +51,7 @@ async function createUser(log: (msg: string) => void): Promise<number> {
|
|||
|
||||
export async function borrowUser(log: (msg: string) => void) {
|
||||
if (!PRIVILEGED) {
|
||||
return { uid: CUR_UID, cleanup: async () => {} };
|
||||
return { uid: CUR_UID, returnUID: async () => {} };
|
||||
} else {
|
||||
return await lock.acquire("key", async () => {
|
||||
if (availIds === null || nextId === null) {
|
||||
|
@ -65,7 +65,7 @@ export async function borrowUser(log: (msg: string) => void) {
|
|||
}
|
||||
return {
|
||||
uid,
|
||||
cleanup: async () => {
|
||||
returnUID: async () => {
|
||||
await lock.acquire("key", () => {
|
||||
availIds!.push(uid);
|
||||
});
|
||||
|
|
|
@ -3,11 +3,16 @@ import * as process from "process";
|
|||
|
||||
import * as appRoot from "app-root-path";
|
||||
|
||||
interface Options extends SpawnOptions {
|
||||
export interface Options extends SpawnOptions {
|
||||
input?: string;
|
||||
check?: boolean;
|
||||
}
|
||||
|
||||
export interface Context {
|
||||
uid: number;
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export const rijuSystemPrivileged = appRoot.resolve(
|
||||
"system/out/riju-system-privileged"
|
||||
);
|
||||
|
@ -26,7 +31,7 @@ export function getEnv(uuid: string) {
|
|||
};
|
||||
}
|
||||
|
||||
export async function call(
|
||||
export async function run(
|
||||
args: string[],
|
||||
log: (msg: string) => void,
|
||||
options?: Options
|
||||
|
@ -63,26 +68,22 @@ export async function call(
|
|||
});
|
||||
}
|
||||
|
||||
export async function callPrivileged(
|
||||
args: string[],
|
||||
log: (msg: string) => void,
|
||||
options?: Options
|
||||
) {
|
||||
await call([rijuSystemPrivileged].concat(args), log, options);
|
||||
export function privilegedUseradd(uid: number) {
|
||||
return [rijuSystemPrivileged, "useradd", `${uid}`];
|
||||
}
|
||||
|
||||
export async function spawnPrivileged(
|
||||
uid: number,
|
||||
uuid: string,
|
||||
args: string[],
|
||||
log: (msg: string) => void,
|
||||
options?: Options
|
||||
) {
|
||||
options = options || {};
|
||||
options.env = getEnv(uuid);
|
||||
await callPrivileged(
|
||||
["spawn", `${uid}`, `${uuid}`].concat(args),
|
||||
log,
|
||||
options
|
||||
);
|
||||
export function privilegedSetup({ uid, uuid }: Context) {
|
||||
return [rijuSystemPrivileged, "setup", `${uid}`, uuid];
|
||||
}
|
||||
|
||||
export function privilegedSpawn({ uid, uuid }: Context, args: string[]) {
|
||||
return [rijuSystemPrivileged, "spawn", `${uid}`, uuid].concat(args);
|
||||
}
|
||||
|
||||
export function privilegedTeardown({ uid, uuid }: Context) {
|
||||
return [rijuSystemPrivileged, "teardown", `${uid}`, uuid];
|
||||
}
|
||||
|
||||
export function bash(cmdline: string) {
|
||||
return ["bash", "-c", cmdline];
|
||||
}
|
||||
|
|
|
@ -66,12 +66,12 @@ class RijuMessageReader extends AbstractMessageReader {
|
|||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
switch (message?.event) {
|
||||
switch (message && message.event) {
|
||||
case "lspOutput":
|
||||
if (DEBUG) {
|
||||
console.log("RECEIVE LSP:", message?.output);
|
||||
console.log("RECEIVE LSP:", message.output);
|
||||
}
|
||||
this.callback!(message?.output);
|
||||
this.callback!(message.output);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -135,14 +135,15 @@ async function main() {
|
|||
if (DEBUG) {
|
||||
console.log("SEND", message);
|
||||
}
|
||||
socket?.send(JSON.stringify(message));
|
||||
if (socket) {
|
||||
socket.send(JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
|
||||
function tryConnect() {
|
||||
let clientDisposable: Disposable | null = null;
|
||||
let servicesDisposable: Disposable | null = null;
|
||||
let lspLogBuffer = "";
|
||||
let daemonLogBuffer = "";
|
||||
const serviceLogBuffers: { [index: string]: string } = {};
|
||||
console.log("Connecting to server...");
|
||||
socket = new WebSocket(
|
||||
(document.location.protocol === "http:" ? "ws://" : "wss://") +
|
||||
|
@ -162,16 +163,17 @@ async function main() {
|
|||
}
|
||||
if (
|
||||
DEBUG &&
|
||||
message?.event !== "lspOutput" &&
|
||||
message?.event !== "lspLog" &&
|
||||
message?.event !== "daemonLog"
|
||||
message &&
|
||||
message.event !== "lspOutput" &&
|
||||
message.event !== "lspLog" &&
|
||||
message.event !== "daemonLog"
|
||||
) {
|
||||
console.log("RECEIVE:", message);
|
||||
}
|
||||
if (message?.event && message?.event !== "error") {
|
||||
if (message && message.event && message.event !== "error") {
|
||||
retryDelayMs = initialRetryDelayMs;
|
||||
}
|
||||
switch (message?.event) {
|
||||
switch (message && message.event) {
|
||||
case "terminalClear":
|
||||
term.reset();
|
||||
return;
|
||||
|
@ -208,7 +210,11 @@ async function main() {
|
|||
documentSelector: [{ pattern: "**" }],
|
||||
middleware: {
|
||||
workspace: {
|
||||
configuration: (params, token, configuration) => {
|
||||
configuration: (
|
||||
params: any,
|
||||
token: any,
|
||||
configuration: any
|
||||
) => {
|
||||
return Array(
|
||||
(configuration(params, token) as {}[]).length
|
||||
).fill(
|
||||
|
@ -220,7 +226,7 @@ async function main() {
|
|||
initializationOptions: config.lspInit || {},
|
||||
},
|
||||
connectionProvider: {
|
||||
get: (errorHandler, closeHandler) =>
|
||||
get: (errorHandler: any, closeHandler: any) =>
|
||||
Promise.resolve(
|
||||
createConnection(connection, errorHandler, closeHandler)
|
||||
),
|
||||
|
@ -231,38 +237,27 @@ async function main() {
|
|||
case "lspOutput":
|
||||
// Should be handled by RijuMessageReader
|
||||
return;
|
||||
case "lspLog":
|
||||
if (typeof message.output !== "string") {
|
||||
case "serviceLog":
|
||||
if (
|
||||
typeof message.service !== "string" ||
|
||||
typeof message.output !== "string"
|
||||
) {
|
||||
console.error("Unexpected message from server:", message);
|
||||
return;
|
||||
}
|
||||
if (DEBUG) {
|
||||
lspLogBuffer += message.output;
|
||||
while (lspLogBuffer.includes("\n")) {
|
||||
const idx = lspLogBuffer.indexOf("\n");
|
||||
const line = lspLogBuffer.slice(0, idx);
|
||||
lspLogBuffer = lspLogBuffer.slice(idx + 1);
|
||||
console.log(`LSP || ${line}`);
|
||||
let buffer = serviceLogBuffers[message.service] || "";
|
||||
buffer += message.output;
|
||||
while (buffer.includes("\n")) {
|
||||
const idx = buffer.indexOf("\n");
|
||||
const line = buffer.slice(0, idx);
|
||||
buffer = buffer.slice(idx + 1);
|
||||
console.log(`${message.service.toUpperCase()} || ${line}`);
|
||||
}
|
||||
serviceLogBuffers[message.service] = buffer;
|
||||
}
|
||||
return;
|
||||
case "daemonLog":
|
||||
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":
|
||||
case "serviceCrashed":
|
||||
return;
|
||||
default:
|
||||
console.error("Unexpected message from server:", message);
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
"@types/express-ws": "^3.0.0",
|
||||
"@types/lodash": "^4.14.155",
|
||||
"@types/mkdirp": "^1.0.1",
|
||||
"@types/node-cleanup": "^2.1.1",
|
||||
"@types/parse-passwd": "^1.0.0",
|
||||
"@types/rimraf": "^3.0.0",
|
||||
"@types/shell-quote": "^1.7.0",
|
||||
|
@ -30,7 +31,6 @@
|
|||
"monaco-editor": "^0.20.0",
|
||||
"monaco-editor-webpack-plugin": "^1.9.0",
|
||||
"monaco-languageclient": "^0.13.0",
|
||||
"node-cleanup": "^2.1.2",
|
||||
"node-pty": "^0.9.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"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
|
||||
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/
|
||||
rm dhall-json dhall-json-*-x86_64-linux.tar.bz2
|
||||
rm -rf dhall-json dhall-json-*-x86_64-linux.tar.bz2
|
||||
|
||||
# Elixir
|
||||
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
|
||||
export GO111MODULE=on
|
||||
export GOPATH=/tmp/go
|
||||
mv /tmp/go/bin/gopls /usr/bin/gopls
|
||||
rm -rf /tmp/go
|
||||
export GOPATH="$PWD/go"
|
||||
go get golang.org/x/tools/gopls@latest
|
||||
mv go/bin/gopls /usr/bin/gopls
|
||||
rm -rf go
|
||||
|
||||
# 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
|
||||
mv stack-*-linux-x86_64-static/stack /usr/bin/stack
|
||||
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
|
||||
if [[ -x system/out/riju-system-privileged ]]; then
|
||||
system/out/riju-system-privileged teardown "*" || true
|
||||
system/out/riju-system-privileged teardown "*" "*" || true
|
||||
fi
|
||||
chmod a=x,u=rwx /tmp/riju
|
||||
|
|
|
@ -14,7 +14,7 @@ const int MAX_UID = 65000;
|
|||
|
||||
int privileged;
|
||||
|
||||
void die(char *msg)
|
||||
void __attribute__ ((noreturn)) die(char *msg)
|
||||
{
|
||||
fprintf(stderr, "%s\n", msg);
|
||||
exit(1);
|
||||
|
@ -24,8 +24,8 @@ void die_with_usage()
|
|||
{
|
||||
die("usage:\n"
|
||||
" riju-system-privileged useradd UID\n"
|
||||
" riju-system-privileged spawn UID CMDLINE...\n"
|
||||
" riju-system-privileged setup UID UUID\n"
|
||||
" riju-system-privileged spawn UID UUID CMDLINE...\n"
|
||||
" 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)
|
||||
die("asprintf failed");
|
||||
int status = system(cmdline);
|
||||
if (status)
|
||||
if (status != 0)
|
||||
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)
|
||||
die("asprintf failed");
|
||||
status = system(cmdline);
|
||||
if (status)
|
||||
if (status != 0)
|
||||
die("useradd failed");
|
||||
}
|
||||
|
||||
|
@ -97,17 +97,44 @@ void setup(int uid, char *uuid)
|
|||
: "install -d -m 700 /tmp/riju/%2$s", uid, uuid) < 0)
|
||||
die("asprintf failed");
|
||||
int status = system(cmdline);
|
||||
if (status)
|
||||
if (status != 0)
|
||||
die("install failed");
|
||||
}
|
||||
|
||||
void teardown(char *uuid)
|
||||
void teardown(int uid, char *uuid)
|
||||
{
|
||||
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)
|
||||
die("asprintf failed");
|
||||
int status = system(cmdline);
|
||||
if (status)
|
||||
status = system(cmdline);
|
||||
if (status != 0)
|
||||
die("rm failed");
|
||||
}
|
||||
|
||||
|
@ -140,10 +167,11 @@ int main(int argc, char **argv)
|
|||
return 0;
|
||||
}
|
||||
if (!strcmp(argv[1], "teardown")) {
|
||||
if (argc != 3)
|
||||
if (argc != 4)
|
||||
die_with_usage();
|
||||
char *uuid = strcmp(argv[2], "*") ? parseUUID(argv[2]) : "*";
|
||||
teardown(uuid);
|
||||
int uid = strcmp(argv[2], "*") ? parseUID(argv[2]) : -1;
|
||||
char *uuid = strcmp(argv[3], "*") ? parseUUID(argv[3]) : "*";
|
||||
teardown(uid, uuid);
|
||||
return 0;
|
||||
}
|
||||
die_with_usage();
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./frontend/out",
|
||||
"rootDir": "./frontend/src"
|
||||
"rootDir": "./frontend/src",
|
||||
"target": "ES3"
|
||||
},
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["frontend/src"]
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
"resolveJsonModule": true,
|
||||
"rootDir": "./backend/src",
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
"strict": true,
|
||||
"target": "ES5"
|
||||
},
|
||||
"include": ["backend/src"]
|
||||
}
|
||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -882,6 +882,11 @@
|
|||
dependencies:
|
||||
"@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@*":
|
||||
version "14.0.11"
|
||||
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"
|
||||
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:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425"
|
||||
|
|
Loading…
Reference in New Issue