Refactor api.ts, plug resource leaks, fix build

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

View File

@ -2,6 +2,8 @@ FROM ubuntu:focal
ARG UID
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"]

View File

@ -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"]

View File

@ -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

View File

@ -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);
}
};
}

View File

@ -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);

View File

@ -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}`)
);
}

View File

@ -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);
});

View File

@ -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];
}

View File

@ -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);

View File

@ -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",

View File

@ -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

424
scripts/my_init Executable file
View File

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

View File

@ -5,6 +5,6 @@ set -o pipefail
mkdir -p /tmp/riju
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

View File

@ -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();

View File

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

View File

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

View File

@ -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"