Import webserver and get it running
This commit is contained in:
		
							parent
							
								
									967cf770c2
								
							
						
					
					
						commit
						d54d0fb5bb
					
				|  | @ -4,3 +4,4 @@ | |||
| **/.terraform | ||||
| **/build | ||||
| **/node_modules | ||||
| **/out | ||||
|  |  | |||
|  | @ -3,3 +3,4 @@ | |||
| .terraform | ||||
| build | ||||
| node_modules | ||||
| out | ||||
|  |  | |||
							
								
								
									
										47
									
								
								Makefile
								
								
								
								
							
							
						
						
									
										47
									
								
								Makefile
								
								
								
								
							|  | @ -21,13 +21,13 @@ help: | |||
| 		sed -E 's/[.]PHONY: */  make /' | \
 | ||||
| 		sed -E 's/[#]## *(.+)/\n    (\1)\n/' | ||||
| 
 | ||||
| ### Build things locally
 | ||||
| ### Build artifacts locally
 | ||||
| 
 | ||||
| .PHONY: image | ||||
| image: | ||||
| 	@: $${I} | ||||
| ifeq ($(I),composite) | ||||
| 	node src/build-composite-image.js | ||||
| 	node tools/build-composite-image.js | ||||
| else | ||||
| 	docker build . -f docker/$(I)/Dockerfile -t riju:$(I) --pull | ||||
| endif | ||||
|  | @ -47,7 +47,7 @@ pkg: | |||
| 	cd $(BUILD)/src && pkg="$(PWD)/$(BUILD)/pkg" ../build.bash | ||||
| 	fakeroot dpkg-deb --build $(BUILD)/pkg $(BUILD)/$(DEB) | ||||
| 
 | ||||
| ### Run things inside Docker
 | ||||
| ### Manipulate artifacts inside Docker
 | ||||
| 
 | ||||
| VOLUME_MOUNT ?= $(PWD) | ||||
| 
 | ||||
|  | @ -57,7 +57,7 @@ shell: | |||
| ifeq ($(I),admin) | ||||
| 	docker run -it --rm --hostname $(I) -v $(VOLUME_MOUNT):/src -v /var/run/docker.sock:/var/run/docker.sock -v $(HOME)/.aws:/var/riju/.aws:ro -e AWS_REGION -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e VOLUME_MOUNT=$(VOLUME_MOUNT) --network host riju:$(I) | ||||
| else | ||||
| 	docker run -it --rm --hostname $(I) -v $(VOLUME_MOUNT):/src riju:$(I) | ||||
| 	docker run -it --rm --hostname $(I) -v $(VOLUME_MOUNT):/src -p 127.0.0.1:6119:6119 -p 127.0.0.1:6120:6120 riju:$(I) | ||||
| endif | ||||
| 
 | ||||
| .PHONY: install | ||||
|  | @ -66,7 +66,42 @@ install: | |||
| 	[[ -z "$$(ls -A /var/lib/apt/lists)" ]] && sudo apt update | ||||
| 	sudo apt reinstall -y ./$(BUILD)/$(DEB) | ||||
| 
 | ||||
| ### Fetch things from registries
 | ||||
| ### Build and run application code
 | ||||
| 
 | ||||
| .PHONY: frontend | ||||
| frontend: | ||||
| 	npx webpack --mode=production | ||||
| 
 | ||||
| .PHONY: frontend-dev | ||||
| frontend-dev: | ||||
| 	npx webpack --mode=development --watch | ||||
| 
 | ||||
| .PHONY: system | ||||
| system: | ||||
| 	./system/compile.bash | ||||
| 
 | ||||
| .PHONY: system-dev | ||||
| system-dev: | ||||
| 	watchexec -w system/src -n ./system/compile.bash | ||||
| 
 | ||||
| .PHONY: server | ||||
| server: | ||||
| 	node backend/server.js | ||||
| 
 | ||||
| .PHONY: server-dev | ||||
| server-dev: | ||||
| 	watchexec -w backend -r -n node backend/server.js | ||||
| 
 | ||||
| .PHONY: build | ||||
| build: frontend system | ||||
| 
 | ||||
| .PHONY: dev | ||||
| dev: | ||||
| 	make -j2 frontend-dev system-dev server-dev | ||||
| 
 | ||||
| ### Run application code
 | ||||
| 
 | ||||
| ### Fetch artifacts from registries
 | ||||
| 
 | ||||
| .PHONY: pull | ||||
| pull: | ||||
|  | @ -80,7 +115,7 @@ download: | |||
| 	mkdir -p $(BUILD) | ||||
| 	aws s3 cp $(S3_DEB) $(BUILD)/$(DEB) | ||||
| 
 | ||||
| ### Publish things to registries
 | ||||
| ### Publish artifacts to registries
 | ||||
| 
 | ||||
| .PHONY: push | ||||
| push: | ||||
|  |  | |||
|  | @ -0,0 +1,436 @@ | |||
| import { spawn } from "child_process"; | ||||
| import path from "path"; | ||||
| import WebSocket from "ws"; | ||||
| 
 | ||||
| import pty from "node-pty"; | ||||
| import PQueue from "p-queue"; | ||||
| import rpc from "vscode-jsonrpc"; | ||||
| import { v4 as getUUID } from "uuid"; | ||||
| 
 | ||||
| import { langs } from "./langs.js"; | ||||
| import { borrowUser } from "./users.js"; | ||||
| import * as util from "./util.js"; | ||||
| import { bash } from "./util.js"; | ||||
| 
 | ||||
| const allSessions = new Set(); | ||||
| 
 | ||||
| export class Session { | ||||
|   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 }; | ||||
|   } | ||||
| 
 | ||||
|   log = (msg) => this.logPrimitive(`[${this.uuid}] ${msg}`); | ||||
| 
 | ||||
|   constructor(ws, lang, log) { | ||||
|     this.ws = ws; | ||||
|     this.uuid = getUUID(); | ||||
|     this.lang = lang; | ||||
|     this.tearingDown = false; | ||||
|     this.uidInfo = null; | ||||
|     this.term = null; | ||||
|     this.lsp = null; | ||||
|     this.daemon = null; | ||||
|     this.formatter = null; | ||||
|     this.logPrimitive = log; | ||||
|     this.msgQueue = new PQueue({ concurrency: 1 }); | ||||
|     this.log(`Creating session, language ${this.lang}`); | ||||
|   } | ||||
| 
 | ||||
|   run = async (args, options) => { | ||||
|     return await util.run(args, this.log, options); | ||||
|   }; | ||||
| 
 | ||||
|   privilegedSetup = () => util.privilegedSetup(this.context); | ||||
|   privilegedSpawn = (args) => 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()); | ||||
|       if (this.config.setup) { | ||||
|         await this.run(this.privilegedSpawn(bash(this.config.setup))); | ||||
|       } | ||||
|       await this.runCode(); | ||||
|       if (this.config.daemon) { | ||||
|         const daemonArgs = this.privilegedSpawn(bash(this.config.daemon)); | ||||
|         const daemonProc = spawn(daemonArgs[0], daemonArgs.slice(1)); | ||||
|         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("close", (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.lsp.setup) { | ||||
|           await this.run(this.privilegedSpawn(bash(this.config.lsp.setup))); | ||||
|         } | ||||
|         const lspArgs = this.privilegedSpawn(bash(this.config.lsp.start)); | ||||
|         const lspProc = spawn(lspArgs[0], lspArgs.slice(1)); | ||||
|         this.lsp = { | ||||
|           proc: lspProc, | ||||
|           reader: new rpc.StreamMessageReader(lspProc.stdout), | ||||
|           writer: new rpc.StreamMessageWriter(lspProc.stdin), | ||||
|         }; | ||||
|         this.lsp.reader.listen((data) => { | ||||
|           this.send({ event: "lspOutput", output: data }); | ||||
|         }); | ||||
|         lspProc.stderr.on("data", (data) => | ||||
|           this.send({ | ||||
|             event: "serviceLog", | ||||
|             service: "lsp", | ||||
|             output: data.toString("utf8"), | ||||
|           }) | ||||
|         ); | ||||
|         lspProc.on("close", (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", (msg) => | ||||
|         this.msgQueue.add(() => this.receive(msg)) | ||||
|       ); | ||||
|       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) => { | ||||
|     try { | ||||
|       if (this.tearingDown) { | ||||
|         return; | ||||
|       } | ||||
|       this.ws.send(JSON.stringify(msg)); | ||||
|     } catch (err) { | ||||
|       this.log(`Failed to send websocket message: ${err}`); | ||||
|       console.log(err); | ||||
|       await this.teardown(); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   sendError = async (err) => { | ||||
|     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. | ||||
| `,
 | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   logBadMessage = (msg) => { | ||||
|     this.log(`Got malformed message from client: ${JSON.stringify(msg)}`); | ||||
|   }; | ||||
| 
 | ||||
|   receive = async (event) => { | ||||
|     try { | ||||
|       if (this.tearingDown) { | ||||
|         return; | ||||
|       } | ||||
|       let msg; | ||||
|       n; | ||||
|       try { | ||||
|         msg = JSON.parse(event); | ||||
|       } catch (err) { | ||||
|         this.log(`Failed to parse message from client: ${event}`); | ||||
|         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 "formatCode": | ||||
|           if (typeof msg.code !== "string") { | ||||
|             this.logBadMessage(msg); | ||||
|             break; | ||||
|           } | ||||
|           await this.formatCode(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; | ||||
|         case "ensure": | ||||
|           if (!this.config.ensure) { | ||||
|             this.log(`ensure ignored because of missing configuration`); | ||||
|             break; | ||||
|           } | ||||
|           await this.ensure(this.config.ensure); | ||||
|           break; | ||||
|         default: | ||||
|           this.logBadMessage(msg); | ||||
|           break; | ||||
|       } | ||||
|     } catch (err) { | ||||
|       this.log(`Error while handling message from client`); | ||||
|       console.log(err); | ||||
|       this.sendError(err); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   writeCode = async (code) => { | ||||
|     if (this.config.main.includes("/")) { | ||||
|       await this.run( | ||||
|         this.privilegedSpawn([ | ||||
|           "mkdir", | ||||
|           "-p", | ||||
|           path.dirname(`${this.homedir}/${this.config.main}`), | ||||
|         ]) | ||||
|       ); | ||||
|     } | ||||
|     await this.run( | ||||
|       this.privilegedSpawn([ | ||||
|         "sh", | ||||
|         "-c", | ||||
|         `cat > ${path.resolve(this.homedir, this.config.main)}`, | ||||
|       ]), | ||||
|       { input: code } | ||||
|     ); | ||||
|   }; | ||||
| 
 | ||||
|   runCode = async (code) => { | ||||
|     try { | ||||
|       const { | ||||
|         name, | ||||
|         repl, | ||||
|         main, | ||||
|         suffix, | ||||
|         createEmpty, | ||||
|         compile, | ||||
|         run, | ||||
|         template, | ||||
|       } = this.config; | ||||
|       if (this.term) { | ||||
|         const pid = this.term.pty.pid; | ||||
|         const args = this.privilegedSpawn( | ||||
|           bash(`kill -SIGTERM ${pid}; sleep 1; kill -SIGKILL ${pid}`) | ||||
|         ); | ||||
|         spawn(args[0], args.slice(1)); | ||||
|         // Signal to terminalOutput message generator using closure.
 | ||||
|         this.term.live = false; | ||||
|         this.term = null; | ||||
|       } | ||||
|       this.send({ event: "terminalClear" }); | ||||
|       let cmdline; | ||||
|       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 !== undefined ? createEmpty : template; | ||||
|       } | ||||
|       if (code && suffix) { | ||||
|         code += suffix; | ||||
|       } | ||||
|       await this.writeCode(code); | ||||
|       const termArgs = this.privilegedSpawn(bash(cmdline)); | ||||
|       const term = { | ||||
|         pty: pty.spawn(termArgs[0], termArgs.slice(1), { | ||||
|           name: "xterm-color", | ||||
|         }), | ||||
|         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 }); | ||||
|         } | ||||
|       }); | ||||
|       this.term.pty.on("exit", (code, signal) => { | ||||
|         if (term.live) { | ||||
|           this.send({ | ||||
|             event: "serviceFailed", | ||||
|             service: "terminal", | ||||
|             error: `Exited with status ${signal || code}`, | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|     } catch (err) { | ||||
|       this.log(`Error while running user code`); | ||||
|       console.log(err); | ||||
|       this.sendError(err); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   formatCode = async (code) => { | ||||
|     try { | ||||
|       if (!this.config.format) { | ||||
|         this.log("formatCode ignored because format is null"); | ||||
|         return; | ||||
|       } | ||||
|       if (this.formatter) { | ||||
|         const pid = this.formatter.proc.pid; | ||||
|         const args = this.privilegedSpawn( | ||||
|           bash(`kill -SIGTERM ${pid}; sleep 1; kill -SIGKILL ${pid}`) | ||||
|         ); | ||||
|         spawn(args[0], args.slice(1)); | ||||
|         this.formatter.live = false; | ||||
|         this.formatter = null; | ||||
|       } | ||||
|       const args = this.privilegedSpawn(bash(this.config.format.run)); | ||||
|       const formatter = { | ||||
|         proc: spawn(args[0], args.slice(1)), | ||||
|         live: true, | ||||
|         input: code, | ||||
|         output: "", | ||||
|       }; | ||||
|       formatter.proc.stdin.end(code); | ||||
|       formatter.proc.stdout.on("data", (data) => { | ||||
|         if (!formatter.live) return; | ||||
|         formatter.output += data.toString("utf8"); | ||||
|       }); | ||||
|       formatter.proc.stderr.on("data", (data) => { | ||||
|         if (!formatter.live) return; | ||||
|         this.send({ | ||||
|           event: "serviceLog", | ||||
|           service: "formatter", | ||||
|           output: data.toString("utf8"), | ||||
|         }); | ||||
|       }); | ||||
|       formatter.proc.on("close", (code, signal) => { | ||||
|         if (!formatter.live) return; | ||||
|         if (code === 0) { | ||||
|           this.send({ | ||||
|             event: "formattedCode", | ||||
|             code: formatter.output, | ||||
|             originalCode: formatter.input, | ||||
|           }); | ||||
|         } else { | ||||
|           this.send({ | ||||
|             event: "serviceFailed", | ||||
|             service: "formatter", | ||||
|             error: `Exited with status ${signal || code}`, | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|       formatter.proc.on("error", (err) => { | ||||
|         if (!formatter.live) return; | ||||
|         this.send({ | ||||
|           event: "serviceFailed", | ||||
|           service: "formatter", | ||||
|           error: `${err}`, | ||||
|         }); | ||||
|       }); | ||||
|       this.formatter = formatter; | ||||
|     } catch (err) { | ||||
|       this.log(`Error while running code formatter`); | ||||
|       console.log(err); | ||||
|       this.sendError(err); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   ensure = async (cmd) => { | ||||
|     const code = await this.run(this.privilegedSpawn(bash(cmd)), { | ||||
|       check: false, | ||||
|     }); | ||||
|     this.send({ event: "ensured", code }); | ||||
|   }; | ||||
| 
 | ||||
|   teardown = async () => { | ||||
|     try { | ||||
|       if (this.tearingDown) { | ||||
|         return; | ||||
|       } | ||||
|       this.log(`Tearing down session`); | ||||
|       this.tearingDown = true; | ||||
|       allSessions.delete(this); | ||||
|       if (this.uidInfo) { | ||||
|         await this.run(this.privilegedTeardown()); | ||||
|         await this.returnUID(); | ||||
|       } | ||||
|       this.ws.terminate(); | ||||
|     } catch (err) { | ||||
|       this.log(`Error during teardown`); | ||||
|       console.log(err); | ||||
|     } | ||||
|   }; | ||||
| } | ||||
|  | @ -0,0 +1,3 @@ | |||
| import process from "process"; | ||||
| 
 | ||||
| export const PRIVILEGED = process.env.RIJU_PRIVILEGED ? true : false; | ||||
|  | @ -0,0 +1,2 @@ | |||
| // TODO
 | ||||
| export const langs = {}; | ||||
|  | @ -0,0 +1,110 @@ | |||
| import child_process from "child_process"; | ||||
| import process from "process"; | ||||
| 
 | ||||
| import readline from "historic-readline"; | ||||
| import { quote } from "shell-quote"; | ||||
| import rpc from "vscode-jsonrpc"; | ||||
| 
 | ||||
| import { langs } from "./langs"; | ||||
| 
 | ||||
| const args = process.argv.slice(2); | ||||
| 
 | ||||
| function printUsage() { | ||||
|   console.log(`usage: yarn lsp-repl (LANG | CMDLINE...)`); | ||||
| } | ||||
| 
 | ||||
| if (args.length === 0) { | ||||
|   printUsage(); | ||||
|   process.exit(1); | ||||
| } | ||||
| 
 | ||||
| if (["-h", "-help", "--help", "help"].includes(args[0])) { | ||||
|   printUsage(); | ||||
|   process.exit(0); | ||||
| } | ||||
| 
 | ||||
| let cmdline; | ||||
| if (args.length === 1 && langs[args[0]] && langs[args[0]].lsp) { | ||||
|   cmdline = ["bash", "-c", langs[args[0]].lsp.start]; | ||||
| } else { | ||||
|   cmdline = args; | ||||
| } | ||||
| 
 | ||||
| console.error(quote(cmdline)); | ||||
| const proc = child_process.spawn(cmdline[0], cmdline.slice(1)); | ||||
| 
 | ||||
| proc.stderr.on("data", (data) => process.stderr.write(data)); | ||||
| proc.on("close", (code, signal) => { | ||||
|   if (code) { | ||||
|     console.error(`Language server exited with code ${code}`); | ||||
|     process.exit(code); | ||||
|   } else { | ||||
|     console.error(`Language server exited due to signal ${signal}`); | ||||
|     process.exit(1); | ||||
|   } | ||||
| }); | ||||
| proc.on("error", (err) => { | ||||
|   console.error(`Failed to start language server: ${err}`); | ||||
|   process.exit(1); | ||||
| }); | ||||
| 
 | ||||
| const reader = new rpc.StreamMessageReader(proc.stdout); | ||||
| const writer = new rpc.StreamMessageWriter(proc.stdin); | ||||
| 
 | ||||
| reader.listen((data) => { | ||||
|   console.log("<<< " + JSON.stringify(data) + "\n"); | ||||
| }); | ||||
| 
 | ||||
| // https://stackoverflow.com/a/10608048/3538165
 | ||||
| function fixStdoutFor(cli) { | ||||
|   var oldStdout = process.stdout; | ||||
|   var newStdout = Object.create(oldStdout); | ||||
|   newStdout.write = function () { | ||||
|     cli.output.write("\x1b[2K\r"); | ||||
|     var result = oldStdout.write.apply( | ||||
|       this, | ||||
|       Array.prototype.slice.call(arguments) | ||||
|     ); | ||||
|     cli._refreshLine(); | ||||
|     return result; | ||||
|   }; | ||||
|   process.__defineGetter__("stdout", function () { | ||||
|     return newStdout; | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| readline.createInterface({ | ||||
|   input: process.stdin, | ||||
|   output: process.stdout, | ||||
|   path: ".lsp-repl-history", | ||||
|   next: (cli) => { | ||||
|     fixStdoutFor(cli); | ||||
|     cli.setPrompt(">>> "); | ||||
|     cli.on("line", (line) => { | ||||
|       if (line) { | ||||
|         let data; | ||||
|         try { | ||||
|           data = JSON.parse(line); | ||||
|         } catch (err) { | ||||
|           console.error(`Invalid JSON: ${err}`); | ||||
|           cli.prompt(); | ||||
|           return; | ||||
|         } | ||||
|         console.log(); | ||||
|         writer.write(data); | ||||
|       } | ||||
|       cli.prompt(); | ||||
|     }); | ||||
|     cli.on("SIGINT", () => { | ||||
|       console.error("^C"); | ||||
|       cli.write("", { ctrl: true, name: "u" }); | ||||
|       cli.prompt(); | ||||
|     }); | ||||
|     cli.on("close", () => { | ||||
|       console.error(); | ||||
|       process.exit(0); | ||||
|     }); | ||||
|     console.log(); | ||||
|     cli.prompt(); | ||||
|   }, | ||||
| }); | ||||
|  | @ -0,0 +1,56 @@ | |||
| import { spawn } from "child_process"; | ||||
| import fs from "fs"; | ||||
| 
 | ||||
| import { v4 as getUUID } from "uuid"; | ||||
| 
 | ||||
| import { langs } from "./langs"; | ||||
| import { MIN_UID, MAX_UID, borrowUser, ignoreUsers } from "./users"; | ||||
| import { | ||||
|   privilegedSetup, | ||||
|   privilegedSpawn, | ||||
|   privilegedTeardown, | ||||
|   run, | ||||
| } from "./util"; | ||||
| 
 | ||||
| function die(msg) { | ||||
|   console.error(msg); | ||||
|   process.exit(1); | ||||
| } | ||||
| 
 | ||||
| function log(msg) { | ||||
|   console.log(msg); | ||||
| } | ||||
| 
 | ||||
| async function main() { | ||||
|   const dirs = await new Promise((resolve, reject) => | ||||
|     fs.readdir("/tmp/riju", (err, dirs) => (err ? reject(err) : resolve(dirs))) | ||||
|   ); | ||||
|   const uids = ( | ||||
|     await Promise.all( | ||||
|       dirs.map( | ||||
|         (dir) => | ||||
|           new Promise((resolve, reject) => | ||||
|             fs.stat(`/tmp/riju/${dir}`, (err, stat) => | ||||
|               err ? reject(err) : resolve(stat.uid) | ||||
|             ) | ||||
|           ) | ||||
|       ) | ||||
|     ) | ||||
|   ).filter((uid) => uid >= MIN_UID && uid < MAX_UID); | ||||
|   await ignoreUsers(uids, log); | ||||
|   const uuid = getUUID(); | ||||
|   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), { | ||||
|     stdio: "inherit", | ||||
|   }); | ||||
|   await new Promise((resolve, reject) => { | ||||
|     proc.on("error", reject); | ||||
|     proc.on("close", resolve); | ||||
|   }); | ||||
|   await run(privilegedTeardown({ uid, uuid }), log); | ||||
|   await returnUID(); | ||||
| } | ||||
| 
 | ||||
| main().catch(die); | ||||
|  | @ -0,0 +1,113 @@ | |||
| import http from "http"; | ||||
| import https from "https"; | ||||
| import path from "path"; | ||||
| 
 | ||||
| import express from "express"; | ||||
| import ws from "express-ws"; | ||||
| import _ from "lodash"; | ||||
| 
 | ||||
| import * as api from "./api.js"; | ||||
| import { langs } from "./langs.js"; | ||||
| 
 | ||||
| const host = process.env.HOST || "localhost"; | ||||
| const port = parseInt(process.env.PORT || "") || 6119; | ||||
| const tlsPort = parseInt(process.env.TLS_PORT || "") || 6120; | ||||
| const useTLS = process.env.TLS ? true : false; | ||||
| const analyticsEnabled = process.env.ANALYTICS ? true : false; | ||||
| 
 | ||||
| const app = express(); | ||||
| 
 | ||||
| app.set("query parser", (qs) => new URLSearchParams(qs)); | ||||
| app.set("view engine", "ejs"); | ||||
| 
 | ||||
| app.get("/", (_, res) => { | ||||
|   res.render(path.resolve("frontend/pages/index"), { | ||||
|     langs, | ||||
|     analyticsEnabled, | ||||
|   }); | ||||
| }); | ||||
| for (const [lang, { aliases }] of Object.entries(langs)) { | ||||
|   if (aliases) { | ||||
|     for (const alias of aliases) { | ||||
|       app.get(`/${_.escapeRegExp(alias)}`, (_, res) => { | ||||
|         res.redirect(301, `/${lang}`); | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| app.get("/:lang", (req, res) => { | ||||
|   const lang = req.params.lang; | ||||
|   const lowered = lang.toLowerCase(); | ||||
|   if (lowered !== lang) { | ||||
|     res.redirect(301, `/${lowered}`); | ||||
|   } else if (langs[lang]) { | ||||
|     res.render(path.resolve("frontend/pages/app"), { | ||||
|       config: { id: lang, ...langs[lang] }, | ||||
|       analyticsEnabled, | ||||
|     }); | ||||
|   } else { | ||||
|     res.send(`No such language: ${lang}`); | ||||
|   } | ||||
| }); | ||||
| app.use("/css", express.static("frontend/styles")); | ||||
| app.use("/js", express.static("frontend/out")); | ||||
| 
 | ||||
| function addWebsocket(baseApp, httpsServer) { | ||||
|   const app = ws(baseApp, httpsServer).app; | ||||
|   app.ws("/api/v1/ws", (ws, req) => { | ||||
|     const lang = req.query.get("lang"); | ||||
|     if (!lang) { | ||||
|       ws.send( | ||||
|         JSON.stringify({ | ||||
|           event: "error", | ||||
|           errorMessage: "No language specified", | ||||
|         }) | ||||
|       ); | ||||
|       ws.close(); | ||||
|     } else if (!langs[lang]) { | ||||
|       ws.send( | ||||
|         JSON.stringify({ | ||||
|           event: "error", | ||||
|           errorMessage: `No such language: ${lang}`, | ||||
|         }) | ||||
|       ); | ||||
|       ws.close(); | ||||
|     } else { | ||||
|       new api.Session(ws, lang, console.log).setup(); | ||||
|     } | ||||
|   }); | ||||
|   return app; | ||||
| } | ||||
| 
 | ||||
| if (useTLS) { | ||||
|   const httpsServer = https.createServer( | ||||
|     { | ||||
|       key: Buffer.from(process.env.TLS_PRIVATE_KEY || "", "base64").toString( | ||||
|         "ascii" | ||||
|       ), | ||||
|       cert: Buffer.from(process.env.TLS_CERTIFICATE || "", "base64").toString( | ||||
|         "ascii" | ||||
|       ), | ||||
|     }, | ||||
|     app | ||||
|   ); | ||||
|   addWebsocket(app, httpsServer); | ||||
|   httpsServer.listen(tlsPort, host, () => | ||||
|     console.log(`Listening on https://${host}:${tlsPort}`) | ||||
|   ); | ||||
|   const server = http | ||||
|     .createServer((req, res) => { | ||||
|       res.writeHead(301, { | ||||
|         Location: "https://" + req.headers["host"] + req.url, | ||||
|       }); | ||||
|       res.end(); | ||||
|     }) | ||||
|     .listen(port, host, () => | ||||
|       console.log(`Listening on http://${host}:${port}`) | ||||
|     ); | ||||
| } else { | ||||
|   addWebsocket(app, undefined); | ||||
|   const server = app.listen(port, host, () => | ||||
|     console.log(`Listening on http://${host}:${port}`) | ||||
|   ); | ||||
| } | ||||
|  | @ -0,0 +1,746 @@ | |||
| import fs from "fs"; | ||||
| import process from "process"; | ||||
| import { promisify } from "util"; | ||||
| 
 | ||||
| import _ from "lodash"; | ||||
| import { Moment } from "moment"; | ||||
| import moment from "moment"; | ||||
| import PQueue from "p-queue"; | ||||
| import rimraf from "rimraf"; | ||||
| import stripAnsi from "strip-ansi"; | ||||
| import { v4 as getUUID } from "uuid"; | ||||
| 
 | ||||
| import api from "./api"; | ||||
| import { LangConfig, langs } from "./langs"; | ||||
| 
 | ||||
| function parseIntOr(thing, def) { | ||||
|   const num = parseInt(thing); | ||||
|   return Number.isNaN(num) ? def : num; | ||||
| } | ||||
| 
 | ||||
| const TIMEOUT_FACTOR = parseIntOr(process.env.TIMEOUT_FACTOR, 1); | ||||
| const CONCURRENCY = parseIntOr(process.env.CONCURRENCY, 2); | ||||
| const BASE_TIMEOUT_SECS = 5; | ||||
| 
 | ||||
| function findPosition(str, idx) { | ||||
|   const lines = str.substring(0, idx).split("\n"); | ||||
|   const line = lines.length - 1; | ||||
|   const character = lines[lines.length - 1].length; | ||||
|   return { line, character }; | ||||
| } | ||||
| 
 | ||||
| async function sendInput(send, input) { | ||||
|   for (const line of input.split("\n")) { | ||||
|     if (line === "EOF") { | ||||
|       send({ event: "terminalInput", input: "\u0004" }); | ||||
|     } else if (line.startsWith("DELAY:")) { | ||||
|       const delay = parseFloat(line.replace(/DELAY: */, "")); | ||||
|       if (Number.isNaN(delay)) continue; | ||||
|       await new Promise((resolve) => | ||||
|         setTimeout(resolve, delay * 1000 * TIMEOUT_FACTOR) | ||||
|       ); | ||||
|     } else { | ||||
|       send({ event: "terminalInput", input: line + "\r" }); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class Test { | ||||
|   get config() { | ||||
|     return langs[this.lang]; | ||||
|   } | ||||
| 
 | ||||
|   record = (msg) => { | ||||
|     const dur = moment.duration(moment().diff(this.startTime)); | ||||
|     this.messages.push({ time: dur.asSeconds(), ...msg }); | ||||
|   }; | ||||
| 
 | ||||
|   send = (msg) => { | ||||
|     this.ws.onMessage(JSON.stringify(msg)); | ||||
|     this.record(msg); | ||||
|     this.handledMessages += 1; | ||||
|   }; | ||||
| 
 | ||||
|   constructor(lang, type) { | ||||
|     this.lang = lang; | ||||
|     this.type = type; | ||||
|     this.messages = []; | ||||
|     this.timedOut = false; | ||||
|     this.handledMessages = 0; | ||||
|     this.handleUpdate = () => {}; | ||||
|     this.startTime = null; | ||||
|     this.ws = null; | ||||
|   } | ||||
| 
 | ||||
|   getLog = (opts) => { | ||||
|     opts = opts || {}; | ||||
|     return this.messages | ||||
|       .map((msg) => JSON.stringify(msg, null, opts.pretty && 2)) | ||||
|       .join("\n"); | ||||
|   }; | ||||
| 
 | ||||
|   run = async () => { | ||||
|     if ((this.config.skip || []).includes(this.type)) { | ||||
|       return "skipped"; | ||||
|     } | ||||
|     this.startTime = moment(); | ||||
|     let session = null; | ||||
|     let timeout = null; | ||||
|     try { | ||||
|       const that = this; | ||||
|       this.ws = { | ||||
|         on: function (type, handler) { | ||||
|           switch (type) { | ||||
|             case "message": | ||||
|               this.onMessage = handler; | ||||
|               for (const msg of this.messageQueue) { | ||||
|                 this.onMessage(msg); | ||||
|               } | ||||
|               this.messageQueue = []; | ||||
|               break; | ||||
|             case "close": | ||||
|             case "error": | ||||
|               // No need to clean up, we'll call teardown() explicitly.
 | ||||
|               break; | ||||
|             default: | ||||
|               throw new Error(`unexpected websocket handler type: ${type}`); | ||||
|           } | ||||
|         }, | ||||
|         onMessage: function (msg) { | ||||
|           this.messageQueue.push(msg); | ||||
|         }, | ||||
|         messageQueue: [], | ||||
|         send: function (data) { | ||||
|           that.record(JSON.parse(data)); | ||||
|           that.handleUpdate(); | ||||
|         }, | ||||
|         terminate: function () {}, | ||||
|       }; | ||||
|       session = new api.Session(this.ws, this.lang, (msg) => { | ||||
|         this.record({ event: "serverLog", message: msg }); | ||||
|       }); | ||||
|       timeout = setTimeout(() => { | ||||
|         this.timedOut = true; | ||||
|         this.handleUpdate(); | ||||
|       }, (this.config.timeout || BASE_TIMEOUT_SECS) * 1000 * TIMEOUT_FACTOR); | ||||
|       await session.setup(); | ||||
|       switch (this.type) { | ||||
|         case "ensure": | ||||
|           await this.testEnsure(); | ||||
|           break; | ||||
|         case "run": | ||||
|           await this.testRun(); | ||||
|           break; | ||||
|         case "repl": | ||||
|           await this.testRepl(); | ||||
|           break; | ||||
|         case "runrepl": | ||||
|           await this.testRunRepl(); | ||||
|           break; | ||||
|         case "scope": | ||||
|           await this.testScope(); | ||||
|           break; | ||||
|         case "format": | ||||
|           await this.testFormat(); | ||||
|           break; | ||||
|         case "lsp": | ||||
|           await this.testLsp(); | ||||
|           break; | ||||
|         default: | ||||
|           throw new Error(`Unexpected test type: ${this.type}`); | ||||
|       } | ||||
|     } finally { | ||||
|       this.ws = null; | ||||
|       if (timeout) { | ||||
|         clearTimeout(timeout); | ||||
|       } | ||||
|       if (session) { | ||||
|         await session.teardown(); | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   wait = async (desc, handler) => { | ||||
|     return await new Promise((resolve, reject) => { | ||||
|       this.handleUpdate = () => { | ||||
|         if (this.timedOut) { | ||||
|           reject(new Error(`timeout while waiting for ${desc}`)); | ||||
|         } else { | ||||
|           while (this.handledMessages < this.messages.length) { | ||||
|             const msg = this.messages[this.handledMessages]; | ||||
|             const result = handler(msg); | ||||
|             if (![undefined, null, false].includes(result)) { | ||||
|               resolve(result); | ||||
|             } | ||||
|             this.handledMessages += 1; | ||||
|           } | ||||
|         } | ||||
|       }; | ||||
|       this.handleUpdate(); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   waitForOutput = async (pattern, maxLength) => { | ||||
|     pattern = pattern.replace(/\n/g, "\r\n"); | ||||
|     let output = ""; | ||||
|     return await this.wait(`output ${JSON.stringify(pattern)}`, (msg) => { | ||||
|       const prevLength = output.length; | ||||
|       if (msg.event === "terminalOutput") { | ||||
|         // Applying stripAnsi here is wrong because escape sequences
 | ||||
|         // could be split across multiple messages. Who cares?
 | ||||
|         output += stripAnsi(msg.output); | ||||
|       } | ||||
|       if (typeof maxLength === "number") { | ||||
|         return ( | ||||
|           output | ||||
|             .substring(prevLength - maxLength) | ||||
|             .match(new RegExp(pattern)) !== null | ||||
|         ); | ||||
|       } else { | ||||
|         return output.indexOf(pattern, prevLength - pattern.length) != -1; | ||||
|       } | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   testEnsure = async () => { | ||||
|     this.send({ event: "ensure" }); | ||||
|     const code = await this.wait("ensure response", (msg) => { | ||||
|       if (msg.event === "ensured") { | ||||
|         return msg.code; | ||||
|       } | ||||
|     }); | ||||
|     if (code !== 0) { | ||||
|       throw new Error(`ensure failed with code ${code}`); | ||||
|     } | ||||
|   }; | ||||
|   testRun = async () => { | ||||
|     const pattern = this.config.hello || "Hello, world!"; | ||||
|     this.send({ event: "runCode", code: this.config.template }); | ||||
|     if (this.config.helloInput !== undefined) { | ||||
|       sendInput(this.send, this.config.helloInput); | ||||
|     } | ||||
|     await this.waitForOutput(pattern, this.config.helloMaxLength); | ||||
|   }; | ||||
|   testRepl = async () => { | ||||
|     const input = this.config.input || "123 * 234"; | ||||
|     const output = this.config.output || "28782"; | ||||
|     sendInput(this.send, input); | ||||
|     await this.waitForOutput(output); | ||||
|   }; | ||||
|   testRunRepl = async () => { | ||||
|     const input = this.config.runReplInput || this.config.input || "123 * 234"; | ||||
|     const output = this.config.runReplOutput || this.config.output || "28782"; | ||||
|     this.send({ event: "runCode", code: this.config.template }); | ||||
|     sendInput(this.send, input); | ||||
|     await this.waitForOutput(output); | ||||
|   }; | ||||
|   testScope = async () => { | ||||
|     const code = this.config.scope.code; | ||||
|     const after = this.config.scope.after; | ||||
|     const input = this.config.scope.input || "x"; | ||||
|     const output = this.config.scope.output || "28782"; | ||||
|     let allCode = this.config.template; | ||||
|     if (!allCode.endsWith("\n")) { | ||||
|       allCode += "\n"; | ||||
|     } | ||||
|     if (after) { | ||||
|       allCode = allCode.replace(after + "\n", after + "\n" + code + "\n"); | ||||
|     } else { | ||||
|       allCode = allCode + code + "\n"; | ||||
|     } | ||||
|     this.send({ event: "runCode", code: allCode }); | ||||
|     sendInput(this.send, input); | ||||
|     await this.waitForOutput(output); | ||||
|   }; | ||||
|   testFormat = async () => { | ||||
|     const input = this.config.format.input; | ||||
|     const output = this.config.format.output || this.config.template; | ||||
|     this.send({ event: "formatCode", code: input }); | ||||
|     const result = await this.wait("formatter response", (msg) => { | ||||
|       if (msg.event === "formattedCode") { | ||||
|         return msg.code; | ||||
|       } | ||||
|     }); | ||||
|     if (output !== result) { | ||||
|       throw new Error("formatted code did not match"); | ||||
|     } | ||||
|   }; | ||||
|   testLsp = async () => { | ||||
|     const insertedCode = this.config.lsp.code; | ||||
|     const after = this.config.lsp.after; | ||||
|     const item = this.config.lsp.item; | ||||
|     const idx = after | ||||
|       ? this.config.template.indexOf(after) + after.length | ||||
|       : this.config.template.length; | ||||
|     const code = | ||||
|       this.config.template.slice(0, idx) + | ||||
|       insertedCode + | ||||
|       this.config.template.slice(idx); | ||||
|     const root = await this.wait("lspStarted message", (msg) => { | ||||
|       if (msg.event === "lspStarted") { | ||||
|         return msg.root; | ||||
|       } | ||||
|     }); | ||||
|     this.send({ | ||||
|       event: "lspInput", | ||||
|       input: { | ||||
|         jsonrpc: "2.0", | ||||
|         id: "0d75333a-47d8-4da8-8030-c81d7bd9eed7", | ||||
|         method: "initialize", | ||||
|         params: { | ||||
|           processId: null, | ||||
|           clientInfo: { name: "vscode" }, | ||||
|           rootPath: root, | ||||
|           rootUri: `file://${root}`, | ||||
|           capabilities: { | ||||
|             workspace: { | ||||
|               applyEdit: true, | ||||
|               workspaceEdit: { | ||||
|                 documentChanges: true, | ||||
|                 resourceOperations: ["create", "rename", "delete"], | ||||
|                 failureHandling: "textOnlyTransactional", | ||||
|               }, | ||||
|               didChangeConfiguration: { dynamicRegistration: true }, | ||||
|               didChangeWatchedFiles: { dynamicRegistration: true }, | ||||
|               symbol: { | ||||
|                 dynamicRegistration: true, | ||||
|                 symbolKind: { | ||||
|                   valueSet: [ | ||||
|                     1, | ||||
|                     2, | ||||
|                     3, | ||||
|                     4, | ||||
|                     5, | ||||
|                     6, | ||||
|                     7, | ||||
|                     8, | ||||
|                     9, | ||||
|                     10, | ||||
|                     11, | ||||
|                     12, | ||||
|                     13, | ||||
|                     14, | ||||
|                     15, | ||||
|                     16, | ||||
|                     17, | ||||
|                     18, | ||||
|                     19, | ||||
|                     20, | ||||
|                     21, | ||||
|                     22, | ||||
|                     23, | ||||
|                     24, | ||||
|                     25, | ||||
|                     26, | ||||
|                   ], | ||||
|                 }, | ||||
|               }, | ||||
|               executeCommand: { dynamicRegistration: true }, | ||||
|               configuration: true, | ||||
|               workspaceFolders: true, | ||||
|             }, | ||||
|             textDocument: { | ||||
|               publishDiagnostics: { | ||||
|                 relatedInformation: true, | ||||
|                 versionSupport: false, | ||||
|                 tagSupport: { valueSet: [1, 2] }, | ||||
|               }, | ||||
|               synchronization: { | ||||
|                 dynamicRegistration: true, | ||||
|                 willSave: true, | ||||
|                 willSaveWaitUntil: true, | ||||
|                 didSave: true, | ||||
|               }, | ||||
|               completion: { | ||||
|                 dynamicRegistration: true, | ||||
|                 contextSupport: true, | ||||
|                 completionItem: { | ||||
|                   snippetSupport: true, | ||||
|                   commitCharactersSupport: true, | ||||
|                   documentationFormat: ["markdown", "plaintext"], | ||||
|                   deprecatedSupport: true, | ||||
|                   preselectSupport: true, | ||||
|                   tagSupport: { valueSet: [1] }, | ||||
|                 }, | ||||
|                 completionItemKind: { | ||||
|                   valueSet: [ | ||||
|                     1, | ||||
|                     2, | ||||
|                     3, | ||||
|                     4, | ||||
|                     5, | ||||
|                     6, | ||||
|                     7, | ||||
|                     8, | ||||
|                     9, | ||||
|                     10, | ||||
|                     11, | ||||
|                     12, | ||||
|                     13, | ||||
|                     14, | ||||
|                     15, | ||||
|                     16, | ||||
|                     17, | ||||
|                     18, | ||||
|                     19, | ||||
|                     20, | ||||
|                     21, | ||||
|                     22, | ||||
|                     23, | ||||
|                     24, | ||||
|                     25, | ||||
|                   ], | ||||
|                 }, | ||||
|               }, | ||||
|               hover: { | ||||
|                 dynamicRegistration: true, | ||||
|                 contentFormat: ["markdown", "plaintext"], | ||||
|               }, | ||||
|               signatureHelp: { | ||||
|                 dynamicRegistration: true, | ||||
|                 signatureInformation: { | ||||
|                   documentationFormat: ["markdown", "plaintext"], | ||||
|                   parameterInformation: { labelOffsetSupport: true }, | ||||
|                 }, | ||||
|                 contextSupport: true, | ||||
|               }, | ||||
|               definition: { dynamicRegistration: true, linkSupport: true }, | ||||
|               references: { dynamicRegistration: true }, | ||||
|               documentHighlight: { dynamicRegistration: true }, | ||||
|               documentSymbol: { | ||||
|                 dynamicRegistration: true, | ||||
|                 symbolKind: { | ||||
|                   valueSet: [ | ||||
|                     1, | ||||
|                     2, | ||||
|                     3, | ||||
|                     4, | ||||
|                     5, | ||||
|                     6, | ||||
|                     7, | ||||
|                     8, | ||||
|                     9, | ||||
|                     10, | ||||
|                     11, | ||||
|                     12, | ||||
|                     13, | ||||
|                     14, | ||||
|                     15, | ||||
|                     16, | ||||
|                     17, | ||||
|                     18, | ||||
|                     19, | ||||
|                     20, | ||||
|                     21, | ||||
|                     22, | ||||
|                     23, | ||||
|                     24, | ||||
|                     25, | ||||
|                     26, | ||||
|                   ], | ||||
|                 }, | ||||
|                 hierarchicalDocumentSymbolSupport: true, | ||||
|               }, | ||||
|               codeAction: { | ||||
|                 dynamicRegistration: true, | ||||
|                 isPreferredSupport: true, | ||||
|                 codeActionLiteralSupport: { | ||||
|                   codeActionKind: { | ||||
|                     valueSet: [ | ||||
|                       "", | ||||
|                       "quickfix", | ||||
|                       "refactor", | ||||
|                       "refactor.extract", | ||||
|                       "refactor.inline", | ||||
|                       "refactor.rewrite", | ||||
|                       "source", | ||||
|                       "source.organizeImports", | ||||
|                     ], | ||||
|                   }, | ||||
|                 }, | ||||
|               }, | ||||
|               codeLens: { dynamicRegistration: true }, | ||||
|               formatting: { dynamicRegistration: true }, | ||||
|               rangeFormatting: { dynamicRegistration: true }, | ||||
|               onTypeFormatting: { dynamicRegistration: true }, | ||||
|               rename: { dynamicRegistration: true, prepareSupport: true }, | ||||
|               documentLink: { dynamicRegistration: true, tooltipSupport: true }, | ||||
|               typeDefinition: { dynamicRegistration: true, linkSupport: true }, | ||||
|               implementation: { dynamicRegistration: true, linkSupport: true }, | ||||
|               colorProvider: { dynamicRegistration: true }, | ||||
|               foldingRange: { | ||||
|                 dynamicRegistration: true, | ||||
|                 rangeLimit: 5000, | ||||
|                 lineFoldingOnly: true, | ||||
|               }, | ||||
|               declaration: { dynamicRegistration: true, linkSupport: true }, | ||||
|             }, | ||||
|           }, | ||||
|           initializationOptions: this.config.lsp.init || {}, | ||||
|           trace: "off", | ||||
|           workspaceFolders: [ | ||||
|             { | ||||
|               uri: `file://${root}`, | ||||
|               name: `file://${root}`, | ||||
|             }, | ||||
|           ], | ||||
|         }, | ||||
|       }, | ||||
|     }); | ||||
|     await this.wait("response to lsp initialize", (msg) => { | ||||
|       return ( | ||||
|         msg.event === "lspOutput" && | ||||
|         msg.output.id === "0d75333a-47d8-4da8-8030-c81d7bd9eed7" | ||||
|       ); | ||||
|     }); | ||||
|     this.send({ | ||||
|       event: "lspInput", | ||||
|       input: { jsonrpc: "2.0", method: "initialized", params: {} }, | ||||
|     }); | ||||
|     this.send({ | ||||
|       event: "lspInput", | ||||
|       input: { | ||||
|         jsonrpc: "2.0", | ||||
|         method: "textDocument/didOpen", | ||||
|         params: { | ||||
|           textDocument: { | ||||
|             uri: `file://${root}/${this.config.main}`, | ||||
|             languageId: | ||||
|               this.config.lsp.lang || this.config.monacoLang || "plaintext", | ||||
|             version: 1, | ||||
|             text: code, | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|     }); | ||||
|     this.send({ | ||||
|       event: "lspInput", | ||||
|       input: { | ||||
|         jsonrpc: "2.0", | ||||
|         id: "ecdb8a55-f755-4553-ae8e-91d6ebbc2045", | ||||
|         method: "textDocument/completion", | ||||
|         params: { | ||||
|           textDocument: { | ||||
|             uri: `file://${root}/${this.config.main}`, | ||||
|           }, | ||||
|           position: findPosition(code, idx + insertedCode.length), | ||||
|           context: { triggerKind: 1 }, | ||||
|         }, | ||||
|       }, | ||||
|     }); | ||||
|     const items = await this.wait( | ||||
|       "response to lsp completion request", | ||||
|       (msg) => { | ||||
|         if (msg.event === "lspOutput") { | ||||
|           if (msg.output.method === "workspace/configuration") { | ||||
|             this.send({ | ||||
|               event: "lspInput", | ||||
|               input: { | ||||
|                 jsonrpc: "2.0", | ||||
|                 id: msg.output.id, | ||||
|                 result: Array(msg.output.params.items.length).fill( | ||||
|                   this.config.lsp.config == undefined | ||||
|                     ? this.config.lsp.config | ||||
|                     : {} | ||||
|                 ), | ||||
|               }, | ||||
|             }); | ||||
|           } else if (msg.output.id === "ecdb8a55-f755-4553-ae8e-91d6ebbc2045") { | ||||
|             return msg.output.result.items || msg.output.result; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     ); | ||||
|     if (!(items && items.filter(({ label }) => label === item).length > 0)) { | ||||
|       throw new Error("completion item did not appear"); | ||||
|     } | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| function lint(lang) { | ||||
|   const config = langs[lang]; | ||||
|   if (!config.template.endsWith("\n")) { | ||||
|     throw new Error("template is missing a trailing newline"); | ||||
|   } | ||||
|   // These can be removed when the types are adjusted to make these
 | ||||
|   // situations impossible.
 | ||||
|   if ( | ||||
|     config.format && | ||||
|     !config.format.input && | ||||
|     !(config.skip || []).includes("format") | ||||
|   ) { | ||||
|     throw new Error("formatter is missing test"); | ||||
|   } | ||||
|   if ( | ||||
|     config.lsp && | ||||
|     !(config.lsp.code && config.lsp.item) && | ||||
|     !(config.skip || []).includes("lsp") | ||||
|   ) { | ||||
|     throw new Error("LSP is missing test"); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const testTypes = { | ||||
|   ensure: { | ||||
|     pred: ({ ensure }) => (ensure ? true : false), | ||||
|   }, | ||||
|   run: { pred: (config) => true }, | ||||
|   repl: { | ||||
|     pred: ({ repl }) => (repl ? true : false), | ||||
|   }, | ||||
|   runrepl: { | ||||
|     pred: ({ repl }) => (repl ? true : false), | ||||
|   }, | ||||
|   scope: { | ||||
|     pred: ({ scope }) => (scope ? true : false), | ||||
|   }, | ||||
|   format: { | ||||
|     pred: ({ format }) => (format ? true : false), | ||||
|   }, | ||||
|   lsp: { pred: ({ lsp }) => (lsp ? true : false) }, | ||||
| }; | ||||
| 
 | ||||
| function getTestList() { | ||||
|   const tests = []; | ||||
|   for (const [id, cfg] of Object.entries(langs)) { | ||||
|     for (const [type, { pred }] of Object.entries(testTypes)) { | ||||
|       if (pred(cfg)) { | ||||
|         tests.push({ lang: id, type }); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   return tests; | ||||
| } | ||||
| 
 | ||||
| async function writeLog(lang, type, result, log) { | ||||
|   log = `${result.toUpperCase()}: ${lang}/${type}\n` + log; | ||||
|   await promisify(fs.mkdir)(`tests/${lang}`, { recursive: true }); | ||||
|   await promisify(fs.writeFile)(`tests/${lang}/${type}.log`, log); | ||||
|   await promisify(fs.mkdir)(`tests-run/${lang}`, { recursive: true }); | ||||
|   await promisify(fs.symlink)( | ||||
|     `../../tests/${lang}/${type}.log`, | ||||
|     `tests-run/${lang}/${type}.log` | ||||
|   ); | ||||
|   await promisify(fs.mkdir)(`tests-${result}/${lang}`, { recursive: true }); | ||||
|   await promisify(fs.symlink)( | ||||
|     `../../tests/${lang}/${type}.log`, | ||||
|     `tests-${result}/${lang}/${type}.log` | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| async function main() { | ||||
|   let tests = getTestList(); | ||||
|   const args = process.argv.slice(2); | ||||
|   for (const arg of args) { | ||||
|     tests = tests.filter( | ||||
|       ({ lang, type }) => | ||||
|         arg | ||||
|           .split(",") | ||||
|           .filter((arg) => | ||||
|             [lang, type].concat(langs[lang].aliases || []).includes(arg) | ||||
|           ).length > 0 | ||||
|     ); | ||||
|   } | ||||
|   if (tests.length === 0) { | ||||
|     console.error("no tests selected"); | ||||
|     process.exit(1); | ||||
|   } | ||||
|   console.error(`Running ${tests.length} test${tests.length !== 1 ? "s" : ""}`); | ||||
|   const lintSeen = new Set(); | ||||
|   let lintPassed = new Set(); | ||||
|   let lintFailed = new Map(); | ||||
|   for (const { lang } of tests) { | ||||
|     if (!lintSeen.has(lang)) { | ||||
|       lintSeen.add(lang); | ||||
|       try { | ||||
|         lint(lang); | ||||
|         lintPassed.add(lang); | ||||
|       } catch (err) { | ||||
|         lintFailed.set(lang, err); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   if (lintFailed.size > 0) { | ||||
|     console.error( | ||||
|       `Language${lintFailed.size !== 1 ? "s" : ""} failed linting:` | ||||
|     ); | ||||
|     console.error( | ||||
|       Array.from(lintFailed) | ||||
|         .map(([lang, err]) => `  - ${lang} (${err})`) | ||||
|         .join("\n") | ||||
|     ); | ||||
|     process.exit(1); | ||||
|   } | ||||
|   await promisify(rimraf)("tests-run"); | ||||
|   await promisify(rimraf)("tests-passed"); | ||||
|   await promisify(rimraf)("tests-skipped"); | ||||
|   await promisify(rimraf)("tests-failed"); | ||||
|   const queue = new PQueue({ concurrency: CONCURRENCY }); | ||||
|   let passed = new Set(); | ||||
|   let skipped = new Set(); | ||||
|   let failed = new Map(); | ||||
|   for (const { lang, type } of tests) { | ||||
|     queue.add(async () => { | ||||
|       const test = new Test(lang, type); | ||||
|       let err; | ||||
|       try { | ||||
|         err = await test.run(); | ||||
|       } catch (error) { | ||||
|         err = error; | ||||
|       } | ||||
|       if (err === "skipped") { | ||||
|         skipped.add({ lang, type }); | ||||
|         console.error(`SKIPPED: ${lang}/${type}`); | ||||
|         await writeLog(lang, type, "skipped", ""); | ||||
|       } else if (!err) { | ||||
|         passed.add({ lang, type }); | ||||
|         console.error(`PASSED: ${lang}/${type}`); | ||||
|         await writeLog( | ||||
|           lang, | ||||
|           type, | ||||
|           "passed", | ||||
|           test.getLog({ pretty: true }) + "\n" | ||||
|         ); | ||||
|       } else { | ||||
|         failed.set({ lang, type }, err); | ||||
|         console.error(`FAILED: ${lang}/${type}`); | ||||
|         console.error(test.getLog()); | ||||
|         console.error(err); | ||||
|         await writeLog( | ||||
|           lang, | ||||
|           type, | ||||
|           "failed", | ||||
|           test.getLog({ pretty: true }) + | ||||
|             "\n" + | ||||
|             (err.stack ? err.stack + "\n" : err ? `${err}` : "") | ||||
|         ); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|   await queue.onIdle(); | ||||
|   console.error(); | ||||
|   console.error( | ||||
|     "================================================================================" | ||||
|   ); | ||||
|   console.error(); | ||||
|   if (passed.size > 0) { | ||||
|     console.error(`${passed.size} test${passed.size !== 1 ? "s" : ""} PASSED`); | ||||
|   } | ||||
|   if (skipped.size > 0) { | ||||
|     console.error( | ||||
|       `${skipped.size} test${skipped.size !== 1 ? "s" : ""} SKIPPED` | ||||
|     ); | ||||
|   } | ||||
|   if (failed.size > 0) { | ||||
|     console.error(`${failed.size} test${failed.size !== 1 ? "s" : ""} FAILED`); | ||||
|     _.sortBy(Array.from(failed), [ | ||||
|       ([{ lang }, _]) => lang, | ||||
|       ([{ type }, _]) => type, | ||||
|     ]).forEach(([{ lang, type }, err]) => | ||||
|       console.error(`  - ${lang}/${type} (${err})`) | ||||
|     ); | ||||
|   } | ||||
|   process.exit(failed.size > 0 ? 1 : 0); | ||||
| } | ||||
| 
 | ||||
| main().catch(console.error); | ||||
|  | @ -0,0 +1,96 @@ | |||
| import { spawn } from "child_process"; | ||||
| import fs from "fs"; | ||||
| import os from "os"; | ||||
| 
 | ||||
| import AsyncLock from "async-lock"; | ||||
| import _ from "lodash"; | ||||
| import parsePasswd from "parse-passwd"; | ||||
| 
 | ||||
| import { PRIVILEGED } from "./config.js"; | ||||
| import { privilegedUseradd, run } from "./util.js"; | ||||
| 
 | ||||
| // Keep in sync with system/src/riju-system-privileged.c
 | ||||
| export const MIN_UID = 2000; | ||||
| export const MAX_UID = 65000; | ||||
| 
 | ||||
| const CUR_UID = os.userInfo().uid; | ||||
| 
 | ||||
| let availIds = null; | ||||
| let nextId = null; | ||||
| let lock = new AsyncLock(); | ||||
| 
 | ||||
| async function readExistingUsers(log) { | ||||
|   availIds = parsePasswd( | ||||
|     await new Promise((resolve, reject) => | ||||
|       fs.readFile("/etc/passwd", "utf-8", (err, data) => { | ||||
|         if (err) { | ||||
|           reject(err); | ||||
|         } else { | ||||
|           resolve(data); | ||||
|         } | ||||
|       }) | ||||
|     ) | ||||
|   ) | ||||
|     .filter(({ username }) => username.startsWith("riju")) | ||||
|     .map(({ uid }) => parseInt(uid)) | ||||
|     .filter((uid) => !isNaN(uid) && uid >= MIN_UID && uid < MAX_UID) | ||||
|     .reverse(); | ||||
|   nextId = (_.max(availIds) || MIN_UID - 1) + 1; | ||||
|   log(`Found ${availIds.length} existing users, next is riju${nextId}`); | ||||
| } | ||||
| 
 | ||||
| async function createUser(log) { | ||||
|   if (nextId >= MAX_UID) { | ||||
|     throw new Error("too many users"); | ||||
|   } | ||||
|   const uid = nextId; | ||||
|   await run(privilegedUseradd(uid), log); | ||||
|   log(`Created new user with ID ${uid}`); | ||||
|   nextId += 1; | ||||
|   return uid; | ||||
| } | ||||
| 
 | ||||
| export async function ignoreUsers(uids, log) { | ||||
|   await lock.acquire("key", async () => { | ||||
|     if (availIds === null || nextId === null) { | ||||
|       await readExistingUsers(log); | ||||
|     } | ||||
|     const uidSet = new Set(uids); | ||||
|     if (uidSet.size > 0) { | ||||
|       const plural = uidSet.size !== 1 ? "s" : ""; | ||||
|       log( | ||||
|         `Ignoring user${plural} from open session${plural}: ${Array.from(uidSet) | ||||
|           .sort() | ||||
|           .map((uid) => `riju${uid}`) | ||||
|           .join(", ")}` | ||||
|       ); | ||||
|     } | ||||
|     availIds = availIds.filter((uid) => !uidSet.has(uid)); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| export async function borrowUser(log) { | ||||
|   if (!PRIVILEGED) { | ||||
|     return { uid: CUR_UID, returnUID: async () => {} }; | ||||
|   } else { | ||||
|     return await lock.acquire("key", async () => { | ||||
|       if (availIds === null || nextId === null) { | ||||
|         await readExistingUsers(log); | ||||
|       } | ||||
|       let uid; | ||||
|       if (availIds.length > 0) { | ||||
|         uid = availIds.pop(); | ||||
|       } else { | ||||
|         uid = await createUser(log); | ||||
|       } | ||||
|       return { | ||||
|         uid, | ||||
|         returnUID: async () => { | ||||
|           await lock.acquire("key", () => { | ||||
|             availIds.push(uid); | ||||
|           }); | ||||
|         }, | ||||
|       }; | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,124 @@ | |||
| import { spawn, spawnSync } from "child_process"; | ||||
| import os from "os"; | ||||
| import process from "process"; | ||||
| 
 | ||||
| import { quote } from "shell-quote"; | ||||
| 
 | ||||
| import { PRIVILEGED } from "./config.js"; | ||||
| import { MIN_UID, MAX_UID } from "./users.js"; | ||||
| 
 | ||||
| export const rijuSystemPrivileged = "system/out/riju-system-privileged"; | ||||
| 
 | ||||
| const rubyVersion = (() => { | ||||
|   try { | ||||
|     return spawnSync("ruby", ["-e", "puts RUBY_VERSION"]) | ||||
|       .stdout.toString() | ||||
|       .trim(); | ||||
|   } catch (err) { | ||||
|     return null; | ||||
|   } | ||||
| })(); | ||||
| 
 | ||||
| function getEnv({ uid, uuid }) { | ||||
|   const cwd = `/tmp/riju/${uuid}`; | ||||
|   const path = [ | ||||
|     rubyVersion && `${cwd}/.gem/ruby/${rubyVersion}/bin`, | ||||
|     `${cwd}/.local/bin`, | ||||
|     `${cwd}/node_modules/.bin`, | ||||
|     `/usr/local/sbin`, | ||||
|     `/usr/local/bin`, | ||||
|     `/usr/sbin`, | ||||
|     `/usr/bin`, | ||||
|     `/bin`, | ||||
|   ].filter((x) => x); | ||||
|   const username = | ||||
|     uid >= MIN_UID && uid < MAX_UID ? `riju${uid}` : os.userInfo().username; | ||||
|   return { | ||||
|     HOME: cwd, | ||||
|     HOSTNAME: "riju", | ||||
|     LANG: process.env.LANG || "", | ||||
|     LC_ALL: process.env.LC_ALL || "", | ||||
|     LOGNAME: username, | ||||
|     PATH: PRIVILEGED ? path.join(":") : process.env.PATH || "", | ||||
|     PWD: cwd, | ||||
|     SHELL: "/usr/bin/bash", | ||||
|     TERM: "xterm-256color", | ||||
|     TMPDIR: `${cwd}`, | ||||
|     USER: username, | ||||
|     USERNAME: username, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| function getEnvString(ctx) { | ||||
|   return Object.entries(getEnv(ctx)) | ||||
|     .map(([key, val]) => `${key}=${quote([val])}`) | ||||
|     .join(" "); | ||||
| } | ||||
| 
 | ||||
| export async function run(args, log, options) { | ||||
|   options = options || {}; | ||||
|   const input = options.input; | ||||
|   const check = options.check === undefined ? true : options.check; | ||||
|   delete options.input; | ||||
|   delete options.check; | ||||
|   const proc = spawn(args[0], args.slice(1), options); | ||||
|   if (typeof input === "string") { | ||||
|     proc.stdin.end(input); | ||||
|   } | ||||
|   let output = ""; | ||||
|   proc.stdout.on("data", (data) => { | ||||
|     output += `${data}`; | ||||
|   }); | ||||
|   proc.stderr.on("data", (data) => { | ||||
|     output += `${data}`; | ||||
|   }); | ||||
|   return await new Promise((resolve, reject) => { | ||||
|     proc.on("error", reject); | ||||
|     proc.on("close", (code, signal) => { | ||||
|       output = output.trim(); | ||||
|       if (output) { | ||||
|         log(`Output from ${args[0]}:\n` + output); | ||||
|       } | ||||
|       if (code === 0 || !check) { | ||||
|         resolve(code); | ||||
|       } else { | ||||
|         reject(`command ${args[0]} failed with error code ${signal || code}`); | ||||
|       } | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| export function privilegedUseradd(uid) { | ||||
|   return [rijuSystemPrivileged, "useradd", `${uid}`]; | ||||
| } | ||||
| 
 | ||||
| export function privilegedSetup({ uid, uuid }) { | ||||
|   return [rijuSystemPrivileged, "setup", `${uid}`, uuid]; | ||||
| } | ||||
| 
 | ||||
| export function privilegedSpawn(ctx, args) { | ||||
|   const { uid, uuid } = ctx; | ||||
|   return [ | ||||
|     rijuSystemPrivileged, | ||||
|     "spawn", | ||||
|     `${uid}`, | ||||
|     uuid, | ||||
|     "sh", | ||||
|     "-c", | ||||
|     `exec env -i ${getEnvString(ctx)} "$@"`, | ||||
|     "--", | ||||
|   ].concat(args); | ||||
| } | ||||
| 
 | ||||
| export function privilegedTeardown({ uid, uuid }) { | ||||
|   return [rijuSystemPrivileged, "teardown", `${uid}`, uuid]; | ||||
| } | ||||
| 
 | ||||
| export function bash(cmdline) { | ||||
|   if (!cmdline.match(/[;|&(){}=]/)) { | ||||
|     // Reduce number of subshells we generate, if we're just running a
 | ||||
|     // single command (no shell logic).
 | ||||
|     cmdline = "exec " + cmdline; | ||||
|   } | ||||
|   return ["bash", "-c", cmdline]; | ||||
| } | ||||
|  | @ -4,6 +4,6 @@ COPY docker/admin/install.bash /tmp/ | |||
| RUN /tmp/install.bash | ||||
| 
 | ||||
| WORKDIR /src | ||||
| COPY docker/admin/pid1.bash /usr/local/sbin/ | ||||
| ENTRYPOINT ["/usr/local/sbin/pid1.bash"] | ||||
| COPY docker/shared/my_init docker/admin/pid1.bash /usr/local/sbin/ | ||||
| ENTRYPOINT ["/usr/local/sbin/my_init", "/usr/local/sbin/pid1.bash"] | ||||
| CMD ["bash"] | ||||
|  |  | |||
|  | @ -27,7 +27,7 @@ deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) sta | |||
| EOF | ||||
| 
 | ||||
| apt-get update | ||||
| apt-get install -y docker-ce-cli less make man nodejs sudo unzip wget yarn | ||||
| apt-get install -y docker-ce-cli less make man nodejs sudo tmux unzip wget yarn | ||||
| 
 | ||||
| wget https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip -O awscli.zip | ||||
| unzip awscli.zip | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| set -euo pipefail | ||||
| 
 | ||||
| groupadd -g "$(stat -c %g "$PWD")" -o -p '!' -r riju | ||||
| useradd -u "$(stat -c %u "$PWD")" -g "$(stat -c %g "$PWD")" -o -m -N -l -s /usr/bin/bash -G sudo riju | ||||
| useradd -u "$(stat -c %u "$PWD")" -g "$(stat -c %g "$PWD")" -o -p '!' -m -N -l -s /usr/bin/bash -G sudo riju | ||||
| 
 | ||||
| runuser -u riju -- touch /home/riju/.sudo_as_admin_successful | ||||
| runuser -u riju -- ln -sT /var/riju/.aws /home/riju/.aws | ||||
|  |  | |||
|  | @ -0,0 +1,12 @@ | |||
| FROM riju:compile AS compile | ||||
| FROM riju:composite | ||||
| 
 | ||||
| RUN useradd -p '!' -m -l -s /usr/bin/bash riju | ||||
| 
 | ||||
| WORKDIR /src | ||||
| ENV RIJU_PRIVILEGED=1 | ||||
| 
 | ||||
| COPY --chown=riju:riju --from=compile . ./ | ||||
| RUN chown root:riju system/out/*-privileged && chmod a=,g=rx,u=rwxs system/out/*-privileged | ||||
| 
 | ||||
| USER riju | ||||
|  | @ -0,0 +1,29 @@ | |||
| #!/usr/bin/env bash | ||||
| 
 | ||||
| set -euxo pipefail | ||||
| 
 | ||||
| export DEBIAN_FRONTEND=noninteractive | ||||
| 
 | ||||
| apt-get update | ||||
| apt-get dist-upgrade -y | ||||
| apt-get install -y curl gnupg lsb-release | ||||
| 
 | ||||
| curl -fsSL https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - | ||||
| curl -fsSL https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - | ||||
| 
 | ||||
| ubuntu_ver="$(lsb_release -rs)" | ||||
| ubuntu_name="$(lsb_release -cs)" | ||||
| 
 | ||||
| node_repo="$(curl -sS https://deb.nodesource.com/setup_current.x | grep NODEREPO= | grep -Eo 'node_[0-9]+\.x' | head -n1)" | ||||
| 
 | ||||
| tee -a /etc/apt/sources.list.d/custom.list >/dev/null <<EOF | ||||
| deb [arch=amd64] https://deb.nodesource.com/${node_repo} ${ubuntu_name} main | ||||
| deb [arch=amd64] https://dl.yarnpkg.com/debian/ stable main | ||||
| EOF | ||||
| 
 | ||||
| apt-get update | ||||
| apt-get install -y make nodejs yarn | ||||
| 
 | ||||
| rm -rf /var/lib/apt/lists/* | ||||
| 
 | ||||
| rm "$0" | ||||
|  | @ -0,0 +1,21 @@ | |||
| FROM ubuntu:rolling AS build | ||||
| 
 | ||||
| COPY docker/runtime/install.bash /tmp/ | ||||
| RUN /tmp/install.bash | ||||
| 
 | ||||
| WORKDIR /src | ||||
| ENV RIJU_PRIVILEGED=1 | ||||
| 
 | ||||
| COPY package.json yarn.lock ./ | ||||
| RUN yarn install | ||||
| 
 | ||||
| COPY webpack.config.cjs ./ | ||||
| COPY frontend/src ./frontend/src/ | ||||
| RUN make frontend | ||||
| 
 | ||||
| COPY system ./system/ | ||||
| RUN make system | ||||
| 
 | ||||
| COPY frontend/pages ./frontend/pages/ | ||||
| COPY frontend/styles ./frontend/styles/ | ||||
| COPY backend ./backend/ | ||||
|  | @ -4,6 +4,6 @@ COPY docker/packaging/install.bash /tmp/ | |||
| RUN /tmp/install.bash | ||||
| 
 | ||||
| WORKDIR /src | ||||
| COPY docker/packaging/pid1.bash /usr/local/sbin/ | ||||
| ENTRYPOINT ["/usr/local/sbin/pid1.bash"] | ||||
| COPY docker/shared/my_init docker/packaging/pid1.bash /usr/local/sbin/ | ||||
| ENTRYPOINT ["/usr/local/sbin/my_init", "/usr/local/sbin/pid1.bash"] | ||||
| CMD ["bash"] | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| set -euo pipefail | ||||
| 
 | ||||
| groupadd -g "$(stat -c %g "$PWD")" -o -p '!' -r riju | ||||
| useradd -u "$(stat -c %u "$PWD")" -g "$(stat -c %g "$PWD")" -o -m -N -l -s /usr/bin/bash -G sudo riju | ||||
| useradd -u "$(stat -c %u "$PWD")" -g "$(stat -c %g "$PWD")" -o -p '!' -m -N -l -s /usr/bin/bash -G sudo riju | ||||
| 
 | ||||
| runuser -u riju touch /home/riju/.sudo_as_admin_successful | ||||
| runuser -u riju -- yarn install | ||||
|  |  | |||
|  | @ -4,6 +4,8 @@ COPY docker/runtime/install.bash /tmp/ | |||
| RUN /tmp/install.bash | ||||
| 
 | ||||
| WORKDIR /src | ||||
| COPY docker/runtime/pid1.bash /usr/local/sbin/ | ||||
| ENTRYPOINT ["/usr/local/sbin/pid1.bash"] | ||||
| COPY docker/shared/my_init docker/runtime/pid1.bash /usr/local/sbin/ | ||||
| ENTRYPOINT ["/usr/local/sbin/my_init", "/usr/local/sbin/pid1.bash"] | ||||
| CMD ["bash"] | ||||
| EXPOSE 6119 | ||||
| EXPOSE 6120 | ||||
|  |  | |||
|  | @ -2,6 +2,12 @@ | |||
| 
 | ||||
| set -euxo pipefail | ||||
| 
 | ||||
| latest_release() { | ||||
|     curl -sSL "https://api.github.com/repos/$1/releases/latest" | jq -r .tag_name | ||||
| } | ||||
| 
 | ||||
| pushd /tmp | ||||
| 
 | ||||
| export DEBIAN_FRONTEND=noninteractive | ||||
| 
 | ||||
| apt-get update | ||||
|  | @ -28,7 +34,12 @@ apt-get install -y dctrl-tools | |||
| libicu="$(grep-aptavail -wF Package 'libicu[0-9]+' -s Package -n | head -n1)" | ||||
| 
 | ||||
| apt-get update | ||||
| apt-get install -y less "${libicu}" make man nodejs sudo yarn | ||||
| apt-get install -y less clang jq "${libicu}" make man nodejs sudo tmux wget yarn | ||||
| 
 | ||||
| ver="$(latest_release watchexec/watchexec)" | ||||
| wget "https://github.com/watchexec/watchexec/releases/download/${ver}/watchexec-${ver}-x86_64-unknown-linux-gnu.deb" | ||||
| apt-get install -y ./watchexec-*.deb | ||||
| rm watchexec-*.deb | ||||
| 
 | ||||
| rm -rf /var/lib/apt/lists/* | ||||
| 
 | ||||
|  | @ -36,4 +47,6 @@ tee /etc/sudoers.d/90-riju >/dev/null <<"EOF" | |||
| %sudo ALL=(ALL:ALL) NOPASSWD: ALL | ||||
| EOF | ||||
| 
 | ||||
| popd | ||||
| 
 | ||||
| rm "$0" | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| set -euo pipefail | ||||
| 
 | ||||
| groupadd -g "$(stat -c %g "$PWD")" -o -p '!' -r riju | ||||
| useradd -u "$(stat -c %u "$PWD")" -g "$(stat -c %g "$PWD")" -o -m -N -l -s /usr/bin/bash -G sudo riju | ||||
| useradd -u "$(stat -c %u "$PWD")" -g "$(stat -c %g "$PWD")" -o -p '!' -m -N -l -s /usr/bin/bash -G sudo riju | ||||
| 
 | ||||
| runuser -u riju touch /home/riju/.sudo_as_admin_successful | ||||
| runuser -u riju -- yarn install | ||||
|  |  | |||
|  | @ -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) | ||||
|  | @ -0,0 +1,30 @@ | |||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <meta charset="utf-8" /> | ||||
|     <title><%= config.name %> - Riju</title> | ||||
|     <link | ||||
|       rel="stylesheet" | ||||
|       href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" | ||||
|       integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" | ||||
|       crossorigin="anonymous" | ||||
|     /> | ||||
|     <link rel="stylesheet" href="/css/app.css" /> | ||||
|   </head> | ||||
|   <body> | ||||
|     <div id="app"> | ||||
|       <div id="editor" class="column"></div> | ||||
|       <div id="terminal" class="column"></div> | ||||
|       <button type="button" class="btn btn-success" id="runButton">Run</button> | ||||
|       <button type="button" class="btn btn-info" id="formatButton">Prettify</button> | ||||
|       <a href="/" class="btn btn-secondary" id="backButton">Switch to a different language</a> | ||||
|     </div> | ||||
|     <script> | ||||
|      window.rijuConfig = <%- JSON.stringify(config) %>; | ||||
|     </script> | ||||
|     <script src="/js/app.js"></script> | ||||
|     <% if (analyticsEnabled) { %> | ||||
|       <script src="https://cdn.usefathom.com/script.js" site="JOBZEHJE" defer></script> | ||||
|     <% } %> | ||||
|   </body> | ||||
| </html> | ||||
|  | @ -0,0 +1,33 @@ | |||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <meta charset="utf-8" /> | ||||
|     <title>Riju</title> | ||||
|     <link rel="stylesheet" href="/css/index.css" /> | ||||
|   </head> | ||||
|   <body> | ||||
|     <h1>Riju: <i>fast</i> online playground for every programming language</h1> | ||||
|     <i>Pick your favorite language to get started:</i> | ||||
|     <div class="grid"> | ||||
|       <% for (const [id, {name}] of Object.entries(langs).sort( | ||||
|         ([id1, {name: name1}], [id2, {name: name2}]) => name1.toLowerCase().localeCompare(name2.toLowerCase()))) { %> | ||||
|         <a href=<%= "/" + encodeURIComponent(id) %> class="language"> | ||||
|           <div class="language"> | ||||
|             <%= name %> | ||||
|           </div> | ||||
|         </a> | ||||
|       <% } %> | ||||
|     </div> | ||||
|     <p> | ||||
|       <i> | ||||
|         Created by | ||||
|         <a href="https://github.com/raxod502">Radon Rosborough</a>. | ||||
|         Check out the project | ||||
|         <a href="https://github.com/raxod502/riju">on GitHub</a>. | ||||
|       </i> | ||||
|     </p> | ||||
|     <% if (analyticsEnabled) { %> | ||||
|       <script src="https://cdn.usefathom.com/script.js" site="JOBZEHJE" defer></script> | ||||
|     <% } %> | ||||
|   </body> | ||||
| </html> | ||||
|  | @ -0,0 +1,309 @@ | |||
| import monaco from "monaco-editor"; | ||||
| import { | ||||
|   createConnection, | ||||
|   MonacoLanguageClient, | ||||
|   MonacoServices, | ||||
|   Services, | ||||
| } from "monaco-languageclient"; | ||||
| import { Disposable } from "vscode"; | ||||
| import { createMessageConnection } from "vscode-jsonrpc"; | ||||
| import { | ||||
|   AbstractMessageReader, | ||||
|   DataCallback, | ||||
| } from "vscode-jsonrpc/lib/messageReader.js"; | ||||
| import { AbstractMessageWriter } from "vscode-jsonrpc/lib/messageWriter.js"; | ||||
| import { Message } from "vscode-jsonrpc/lib/messages.js"; | ||||
| import { Terminal } from "xterm"; | ||||
| import { FitAddon } from "xterm-addon-fit"; | ||||
| 
 | ||||
| import "xterm/css/xterm.css"; | ||||
| 
 | ||||
| const DEBUG = window.location.hash === "#debug"; | ||||
| const config = window.rijuConfig; | ||||
| 
 | ||||
| class RijuMessageReader extends AbstractMessageReader { | ||||
|   constructor(socketSocket) { | ||||
|     super(); | ||||
|     this.state = "initial"; | ||||
|     this.callback = null; | ||||
|     this.messageQueue = []; | ||||
|     this.socket = socket; | ||||
|     this.socket.addEventListener("message", (event) => { | ||||
|       this.readMessage(event.data); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   listen(callback) { | ||||
|     if (this.state === "initial") { | ||||
|       this.state = "listening"; | ||||
|       this.callback = callback; | ||||
|       while (this.messageQueue.length > 0) { | ||||
|         this.readMessage(this.messageQueue.pop()); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   readMessage(rawMessage) { | ||||
|     if (this.state === "initial") { | ||||
|       this.messageQueue.splice(0, 0, rawMessage); | ||||
|     } else if (this.state === "listening") { | ||||
|       let message; | ||||
|       try { | ||||
|         message = JSON.parse(rawMessage); | ||||
|       } catch (err) { | ||||
|         return; | ||||
|       } | ||||
|       switch (message && message.event) { | ||||
|         case "lspOutput": | ||||
|           if (DEBUG) { | ||||
|             console.log("RECEIVE LSP:", message.output); | ||||
|           } | ||||
|           this.callback(message.output); | ||||
|           break; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class RijuMessageWriter extends AbstractMessageWriter { | ||||
|   constructor(socket) { | ||||
|     super(); | ||||
|     this.socket = socket; | ||||
|   } | ||||
| 
 | ||||
|   write(msg) { | ||||
|     switch (msg.method) { | ||||
|       case "initialize": | ||||
|         msg.params.processId = null; | ||||
|         if (config.lsp.disableDynamicRegistration) { | ||||
|           this.disableDynamicRegistration(msg); | ||||
|         } | ||||
|         break; | ||||
|       case "textDocument/didOpen": | ||||
|         if (config.lsp.lang) { | ||||
|           msg.params.textDocument.languageId = config.lsp.lang; | ||||
|         } | ||||
|     } | ||||
|     if (DEBUG) { | ||||
|       console.log("SEND LSP:", msg); | ||||
|     } | ||||
|     this.socket.send(JSON.stringify({ event: "lspInput", input: msg })); | ||||
|   } | ||||
| 
 | ||||
|   disableDynamicRegistration(msg) { | ||||
|     if (!msg || typeof msg !== "object") return; | ||||
|     for (const [key, val] of Object.entries(msg)) { | ||||
|       if (key === "dynamicRegistration" && val === true) | ||||
|         msg.dynamicRegistration = false; | ||||
|       this.disableDynamicRegistration(val); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| async function main() { | ||||
|   const term = new Terminal(); | ||||
|   const fitAddon = new FitAddon(); | ||||
|   term.loadAddon(fitAddon); | ||||
|   term.open(document.getElementById("terminal")); | ||||
| 
 | ||||
|   fitAddon.fit(); | ||||
|   window.addEventListener("resize", () => fitAddon.fit()); | ||||
| 
 | ||||
|   await new Promise((resolve) => | ||||
|     term.write("Connecting to server...", resolve) | ||||
|   ); | ||||
| 
 | ||||
|   const initialRetryDelayMs = 200; | ||||
|   let retryDelayMs = initialRetryDelayMs; | ||||
| 
 | ||||
|   function sendMessage(message) { | ||||
|     if (DEBUG) { | ||||
|       console.log("SEND:", message); | ||||
|     } | ||||
|     if (socket) { | ||||
|       socket.send(JSON.stringify(message)); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function tryConnect() { | ||||
|     let clientDisposable = null; | ||||
|     let servicesDisposable = null; | ||||
|     const serviceLogBuffers = {}; | ||||
|     console.log("Connecting to server..."); | ||||
|     socket = new WebSocket( | ||||
|       (document.location.protocol === "http:" ? "ws://" : "wss://") + | ||||
|         document.location.host + | ||||
|         `/api/v1/ws?lang=${encodeURIComponent(config.id)}` | ||||
|     ); | ||||
|     socket.addEventListener("open", () => { | ||||
|       console.log("Successfully connected to server"); | ||||
|     }); | ||||
|     socket.addEventListener("message", (event) => { | ||||
|       let message; | ||||
|       try { | ||||
|         message = JSON.parse(event.data); | ||||
|       } catch (err) { | ||||
|         console.error("Malformed message from server:", event.data); | ||||
|         return; | ||||
|       } | ||||
|       if ( | ||||
|         DEBUG && | ||||
|         message && | ||||
|         message.event !== "lspOutput" && | ||||
|         message.event !== "serviceLog" | ||||
|       ) { | ||||
|         console.log("RECEIVE:", message); | ||||
|       } | ||||
|       if (message && message.event && message.event !== "error") { | ||||
|         retryDelayMs = initialRetryDelayMs; | ||||
|       } | ||||
|       switch (message && message.event) { | ||||
|         case "terminalClear": | ||||
|           term.reset(); | ||||
|           return; | ||||
|         case "terminalOutput": | ||||
|           if (typeof message.output !== "string") { | ||||
|             console.error("Unexpected message from server:", message); | ||||
|             return; | ||||
|           } | ||||
|           term.write(message.output); | ||||
|           return; | ||||
|         case "formattedCode": | ||||
|           if ( | ||||
|             typeof message.code !== "string" || | ||||
|             typeof message.originalCode !== "string" | ||||
|           ) { | ||||
|             console.error("Unexpected message from server:", message); | ||||
|             return; | ||||
|           } | ||||
|           if (editor.getValue() === message.originalCode) { | ||||
|             editor.setValue(message.code); | ||||
|           } | ||||
|           return; | ||||
|         case "lspStarted": | ||||
|           if (typeof message.root !== "string") { | ||||
|             console.error("Unexpected message from server:", message); | ||||
|             return; | ||||
|           } | ||||
|           const services = MonacoServices.create(editor, { | ||||
|             rootUri: `file://${message.root}`, | ||||
|           }); | ||||
|           servicesDisposable = Services.install(services); | ||||
|           editor.setModel( | ||||
|             monaco.editor.createModel( | ||||
|               editor.getModel().getValue(), | ||||
|               undefined, | ||||
|               monaco.Uri.parse(`file://${message.root}/${config.main}`) | ||||
|             ) | ||||
|           ); | ||||
|           const connection = createMessageConnection( | ||||
|             new RijuMessageReader(socket), | ||||
|             new RijuMessageWriter(socket) | ||||
|           ); | ||||
|           const client = new MonacoLanguageClient({ | ||||
|             name: "Riju", | ||||
|             clientOptions: { | ||||
|               documentSelector: [{ pattern: "**" }], | ||||
|               middleware: { | ||||
|                 workspace: { | ||||
|                   configuration: (params, token, configuration) => { | ||||
|                     return Array(configuration(params, token).length).fill( | ||||
|                       config.lsp.config !== undefined ? config.lsp.config : {} | ||||
|                     ); | ||||
|                   }, | ||||
|                 }, | ||||
|               }, | ||||
|               initializationOptions: config.lsp.init || {}, | ||||
|             }, | ||||
|             connectionProvider: { | ||||
|               get: (errorHandler, closeHandler) => | ||||
|                 Promise.resolve( | ||||
|                   createConnection(connection, errorHandler, closeHandler) | ||||
|                 ), | ||||
|             }, | ||||
|           }); | ||||
|           clientDisposable = client.start(); | ||||
|           return; | ||||
|         case "lspOutput": | ||||
|           // Should be handled by RijuMessageReader
 | ||||
|           return; | ||||
|         case "serviceLog": | ||||
|           if ( | ||||
|             typeof message.service !== "string" || | ||||
|             typeof message.output !== "string" | ||||
|           ) { | ||||
|             console.error("Unexpected message from server:", message); | ||||
|             return; | ||||
|           } | ||||
|           if (DEBUG) { | ||||
|             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 "serviceCrashed": | ||||
|           return; | ||||
|         default: | ||||
|           console.error("Unexpected message from server:", message); | ||||
|           return; | ||||
|       } | ||||
|     }); | ||||
|     socket.addEventListener("close", (event) => { | ||||
|       if (event.wasClean) { | ||||
|         console.log("Connection closed cleanly"); | ||||
|       } else { | ||||
|         console.error("Connection died"); | ||||
|       } | ||||
|       if (clientDisposable) { | ||||
|         clientDisposable.dispose(); | ||||
|         clientDisposable = null; | ||||
|       } | ||||
|       if (servicesDisposable) { | ||||
|         servicesDisposable.dispose(); | ||||
|         servicesDisposable = null; | ||||
|       } | ||||
|       scheduleConnect(); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   function scheduleConnect() { | ||||
|     const delay = retryDelayMs * Math.random(); | ||||
|     console.log(`Trying to reconnect in ${Math.floor(delay)}ms`); | ||||
|     setTimeout(tryConnect, delay); | ||||
|     retryDelayMs *= 2; | ||||
|   } | ||||
| 
 | ||||
|   let socket = null; | ||||
|   tryConnect(); | ||||
| 
 | ||||
|   term.onData((data) => sendMessage({ event: "terminalInput", input: data })); | ||||
| 
 | ||||
|   const editor = monaco.editor.create(document.getElementById("editor"), { | ||||
|     minimap: { enabled: false }, | ||||
|     scrollbar: { verticalScrollbarSize: 0 }, | ||||
|   }); | ||||
|   window.addEventListener("resize", () => editor.layout()); | ||||
|   editor.getModel().setValue(config.template); | ||||
|   monaco.editor.setModelLanguage( | ||||
|     editor.getModel(), | ||||
|     config.monacoLang || "plaintext" | ||||
|   ); | ||||
| 
 | ||||
|   document.getElementById("runButton").addEventListener("click", () => { | ||||
|     sendMessage({ event: "runCode", code: editor.getValue() }); | ||||
|   }); | ||||
|   if (config.format) { | ||||
|     document.getElementById("formatButton").classList.add("visible"); | ||||
|     document.getElementById("formatButton").addEventListener("click", () => { | ||||
|       sendMessage({ event: "formatCode", code: editor.getValue() }); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| main().catch(console.error); | ||||
|  | @ -0,0 +1,45 @@ | |||
| body { | ||||
|   margin: 0; | ||||
|   height: 100vh; | ||||
| } | ||||
| 
 | ||||
| #app { | ||||
|   height: 100%; | ||||
| } | ||||
| 
 | ||||
| .column { | ||||
|   width: 50%; | ||||
|   height: 100%; | ||||
|   float: left; | ||||
| } | ||||
| 
 | ||||
| #editor { | ||||
|   overflow: hidden; | ||||
| } | ||||
| 
 | ||||
| #terminal { | ||||
|   background: black; | ||||
| } | ||||
| 
 | ||||
| #runButton { | ||||
|   position: absolute; | ||||
|   top: 25px; | ||||
|   right: calc(50% + 25px); | ||||
| } | ||||
| 
 | ||||
| #formatButton { | ||||
|   position: absolute; | ||||
|   bottom: 25px; | ||||
|   right: calc(50% + 25px); | ||||
|   visibility: hidden; | ||||
| } | ||||
| 
 | ||||
| #formatButton.visible { | ||||
|   visibility: visible; | ||||
| } | ||||
| 
 | ||||
| #backButton { | ||||
|   position: absolute; | ||||
|   left: 25px; | ||||
|   bottom: 25px; | ||||
| } | ||||
|  | @ -0,0 +1,33 @@ | |||
| body { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   align-items: center; | ||||
|   font-family: sans-serif; | ||||
|   text-align: center; | ||||
|   padding-bottom: 20px; | ||||
| } | ||||
| 
 | ||||
| .grid { | ||||
|   display: flex; | ||||
|   flex-wrap: wrap; | ||||
|   justify-content: center; | ||||
|   margin-top: 20px; | ||||
| } | ||||
| 
 | ||||
| div.language { | ||||
|   width: 140px; | ||||
|   height: 60px; | ||||
|   border: solid; | ||||
|   margin: 5px; | ||||
|   padding: 5px; | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   align-items: center; | ||||
|   text-align: center; | ||||
|   font-size: 18px; | ||||
| } | ||||
| 
 | ||||
| a.language { | ||||
|   text-decoration: none; | ||||
|   color: black; | ||||
| } | ||||
							
								
								
									
										23
									
								
								package.json
								
								
								
								
							
							
						
						
									
										23
									
								
								package.json
								
								
								
								
							|  | @ -5,8 +5,31 @@ | |||
|   "private": true, | ||||
|   "type": "module", | ||||
|   "dependencies": { | ||||
|     "@babel/core": "^7.12.10", | ||||
|     "@babel/preset-env": "^7.12.11", | ||||
|     "async-lock": "^1.2.6", | ||||
|     "babel-loader": "^8.2.2", | ||||
|     "commander": "^6.2.1", | ||||
|     "css-loader": "^5.0.1", | ||||
|     "ejs": "^3.1.5", | ||||
|     "express": "^4.17.1", | ||||
|     "express-ws": "^4.0.0", | ||||
|     "file-loader": "^6.2.0", | ||||
|     "lodash": "^4.17.20", | ||||
|     "monaco-editor": "^0.21.2", | ||||
|     "monaco-editor-webpack-plugin": "^2.1.0", | ||||
|     "monaco-languageclient": "^0.13.0", | ||||
|     "node-pty": "^0.9.0", | ||||
|     "p-queue": "^6.6.2", | ||||
|     "parse-passwd": "^1.0.0", | ||||
|     "shell-quote": "^1.7.2", | ||||
|     "style-loader": "^2.0.0", | ||||
|     "uuid": "^8.3.2", | ||||
|     "vscode-languageserver-protocol": "3.15.3", | ||||
|     "webpack": "^5.11.0", | ||||
|     "webpack-cli": "^4.2.0", | ||||
|     "xterm": "^4.9.0", | ||||
|     "xterm-addon-fit": "^0.4.0", | ||||
|     "yaml": "^1.10.0" | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,25 @@ | |||
| #!/usr/bin/env bash | ||||
| 
 | ||||
| set -euo pipefail | ||||
| 
 | ||||
| if [[ ! -d system/src ]]; then | ||||
|     echo "compile.bash: no system/src directory" >&2 | ||||
|     exit 1 | ||||
| fi | ||||
| 
 | ||||
| function verbosely { | ||||
|     echo "$@" | ||||
|     "$@" | ||||
| } | ||||
| 
 | ||||
| mkdir -p system/out | ||||
| rm -f system/out/* | ||||
| for src in system/src/*.c; do | ||||
|     out="${src/src/out}" | ||||
|     out="${out/.c}" | ||||
|     verbosely clang -Wall -Wextra -Werror -std=c11 "${src}" -o "${out}" | ||||
|     if [[ "${out}" == *-privileged && -n "${RIJU_PRIVILEGED:-}" ]]; then | ||||
|         sudo chown root:riju "${out}" | ||||
|         sudo chmod a=,g=rx,u=rwxs "${out}" | ||||
|     fi | ||||
| done | ||||
|  | @ -0,0 +1,178 @@ | |||
| #define _GNU_SOURCE | ||||
| #include <errno.h> | ||||
| #include <grp.h> | ||||
| #include <stdio.h> | ||||
| #include <stdlib.h> | ||||
| #include <string.h> | ||||
| #include <sys/stat.h> | ||||
| #include <sys/types.h> | ||||
| #include <unistd.h> | ||||
| 
 | ||||
| // Keep in sync with backend/src/users.ts
 | ||||
| const int MIN_UID = 2000; | ||||
| const int MAX_UID = 65000; | ||||
| 
 | ||||
| int privileged; | ||||
| 
 | ||||
| void __attribute__ ((noreturn)) die(char *msg) | ||||
| { | ||||
|   fprintf(stderr, "%s\n", msg); | ||||
|   exit(1); | ||||
| } | ||||
| 
 | ||||
| void die_with_usage() | ||||
| { | ||||
|   die("usage:\n" | ||||
|       "  riju-system-privileged useradd UID\n" | ||||
|       "  riju-system-privileged setup UID UUID\n" | ||||
|       "  riju-system-privileged spawn UID UUID CMDLINE...\n" | ||||
|       "  riju-system-privileged teardown UID UUID"); | ||||
| } | ||||
| 
 | ||||
| int parseUID(char *str) | ||||
| { | ||||
|   if (!privileged) | ||||
|     return -1; | ||||
|   char *endptr; | ||||
|   long uid = strtol(str, &endptr, 10); | ||||
|   if (!*str || *endptr) | ||||
|     die("uid must be an integer"); | ||||
|   if (uid < MIN_UID || uid >= MAX_UID) | ||||
|     die("uid is out of range"); | ||||
|   return uid; | ||||
| } | ||||
| 
 | ||||
| char *parseUUID(char *uuid) | ||||
| { | ||||
|   if (!*uuid) | ||||
|     die("illegal uuid"); | ||||
|   for (char *ptr = uuid; *ptr; ++ptr) | ||||
|     if (!((*ptr >= 'a' && *ptr <= 'z') || (*ptr >= '0' && *ptr <= '9') || *ptr == '-')) | ||||
|       die("illegal uuid"); | ||||
|   return uuid; | ||||
| } | ||||
| 
 | ||||
| void useradd(int uid) | ||||
| { | ||||
|   if (!privileged) | ||||
|     die("useradd not allowed without root privileges"); | ||||
|   char *cmdline; | ||||
|   if (asprintf(&cmdline, "groupadd -g %1$d riju%1$d", uid) < 0) | ||||
|     die("asprintf failed"); | ||||
|   int status = system(cmdline); | ||||
|   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 != 0) | ||||
|     die("useradd failed"); | ||||
| } | ||||
| 
 | ||||
| void spawn(int uid, char *uuid, char **cmdline) | ||||
| { | ||||
|   char *cwd; | ||||
|   if (asprintf(&cwd, "/tmp/riju/%s", uuid) < 0) | ||||
|     die("asprintf failed"); | ||||
|   if (chdir(cwd) < 0) | ||||
|     die("chdir failed"); | ||||
|   if (privileged) { | ||||
|     if (setgid(uid) < 0) | ||||
|       die("setgid failed"); | ||||
|     if (setgroups(0, NULL) < 0) | ||||
|       die("setgroups failed"); | ||||
|     if (setuid(uid) < 0) | ||||
|       die("setuid failed"); | ||||
|   } | ||||
|   umask(077); | ||||
|   execvp(cmdline[0], cmdline); | ||||
|   die("execvp failed"); | ||||
| } | ||||
| 
 | ||||
| void setup(int uid, char *uuid) | ||||
| { | ||||
|   char *cmdline; | ||||
|   if (asprintf(&cmdline, privileged | ||||
|                ? "install -d -o riju%1$d -g riju%1$d -m 700 /tmp/riju/%2$s" | ||||
|                : "install -d -m 700 /tmp/riju/%2$s", uid, uuid) < 0) | ||||
|     die("asprintf failed"); | ||||
|   int status = system(cmdline); | ||||
|   if (status != 0) | ||||
|     die("install failed"); | ||||
| } | ||||
| 
 | ||||
| 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, "while pkill -9 --uid %1$s; do sleep 0.01; done", 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"); | ||||
|   status = system(cmdline); | ||||
|   if (status != 0) | ||||
|     die("rm failed"); | ||||
| } | ||||
| 
 | ||||
| int main(int argc, char **argv) | ||||
| { | ||||
|   int code = setuid(0); | ||||
|   if (code != 0 && code != -EPERM) | ||||
|     die("setuid failed"); | ||||
|   privileged = code == 0; | ||||
|   if (argc < 2) | ||||
|     die_with_usage(); | ||||
|   if (!strcmp(argv[1], "useradd")) { | ||||
|     if (argc != 3) | ||||
|       die_with_usage(); | ||||
|     useradd(parseUID(argv[2])); | ||||
|     return 0; | ||||
|   } | ||||
|   if (!strcmp(argv[1], "spawn")) { | ||||
|     if (argc < 5) | ||||
|       die_with_usage(); | ||||
|     spawn(parseUID(argv[2]), parseUUID(argv[3]), &argv[4]); | ||||
|     return 0; | ||||
|   } | ||||
|   if (!strcmp(argv[1], "setup")) { | ||||
|     if (argc != 4) | ||||
|       die_with_usage(); | ||||
|     int uid = parseUID(argv[2]); | ||||
|     char *uuid = parseUUID(argv[3]); | ||||
|     setup(uid, uuid); | ||||
|     return 0; | ||||
|   } | ||||
|   if (!strcmp(argv[1], "teardown")) { | ||||
|     if (argc != 4) | ||||
|       die_with_usage(); | ||||
|     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(); | ||||
| } | ||||
|  | @ -0,0 +1,55 @@ | |||
| const path = require("path"); | ||||
| 
 | ||||
| const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin"); | ||||
| 
 | ||||
| function isProduction(argv) { | ||||
|   return !argv.development; | ||||
| } | ||||
| 
 | ||||
| module.exports = (_, argv) => ({ | ||||
|   devtool: isProduction(argv) ? undefined : "source-map", | ||||
|   entry: "./frontend/src/app.js", | ||||
|   mode: isProduction(argv) ? "production" : "development", | ||||
|   module: { | ||||
|     rules: [ | ||||
|       { | ||||
|         test: /\.css$/i, | ||||
|         use: ["style-loader", "css-loader"], | ||||
|       }, | ||||
|       { | ||||
|         test: /\.ttf$/, | ||||
|         use: ["file-loader"], | ||||
|       }, | ||||
|       { | ||||
|         test: /\.js$/, | ||||
|         use: { | ||||
|           loader: "babel-loader", | ||||
|           options: { | ||||
|             presets: ["@babel/preset-env"], | ||||
|           }, | ||||
|         }, | ||||
|         include: /vscode-jsonrpc/, | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   output: { | ||||
|     path: path.resolve(__dirname, "frontend/out"), | ||||
|     publicPath: "/js/", | ||||
|     filename: "app.js", | ||||
|   }, | ||||
|   performance: { | ||||
|     hints: false, | ||||
|   }, | ||||
|   plugins: [new MonacoWebpackPlugin()], | ||||
|   resolve: { | ||||
|     alias: { | ||||
|       vscode: require.resolve("monaco-languageclient/lib/vscode-compatibility"), | ||||
|     }, | ||||
|     fallback: { | ||||
|       crypto: false, | ||||
|       net: false, | ||||
|       os: false, | ||||
|       path: false, | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
		Loading…
	
		Reference in New Issue
	
	 Radon Rosborough
						Radon Rosborough