Import webserver and get it running
This commit is contained in:
		
							parent
							
								
									967cf770c2
								
							
						
					
					
						commit
						d54d0fb5bb
					
				|  | @ -4,3 +4,4 @@ | ||||||
| **/.terraform | **/.terraform | ||||||
| **/build | **/build | ||||||
| **/node_modules | **/node_modules | ||||||
|  | **/out | ||||||
|  |  | ||||||
|  | @ -3,3 +3,4 @@ | ||||||
| .terraform | .terraform | ||||||
| build | build | ||||||
| node_modules | node_modules | ||||||
|  | out | ||||||
|  |  | ||||||
							
								
								
									
										47
									
								
								Makefile
								
								
								
								
							
							
						
						
									
										47
									
								
								Makefile
								
								
								
								
							|  | @ -21,13 +21,13 @@ help: | ||||||
| 		sed -E 's/[.]PHONY: */  make /' | \
 | 		sed -E 's/[.]PHONY: */  make /' | \
 | ||||||
| 		sed -E 's/[#]## *(.+)/\n    (\1)\n/' | 		sed -E 's/[#]## *(.+)/\n    (\1)\n/' | ||||||
| 
 | 
 | ||||||
| ### Build things locally
 | ### Build artifacts locally
 | ||||||
| 
 | 
 | ||||||
| .PHONY: image | .PHONY: image | ||||||
| image: | image: | ||||||
| 	@: $${I} | 	@: $${I} | ||||||
| ifeq ($(I),composite) | ifeq ($(I),composite) | ||||||
| 	node src/build-composite-image.js | 	node tools/build-composite-image.js | ||||||
| else | else | ||||||
| 	docker build . -f docker/$(I)/Dockerfile -t riju:$(I) --pull | 	docker build . -f docker/$(I)/Dockerfile -t riju:$(I) --pull | ||||||
| endif | endif | ||||||
|  | @ -47,7 +47,7 @@ pkg: | ||||||
| 	cd $(BUILD)/src && pkg="$(PWD)/$(BUILD)/pkg" ../build.bash | 	cd $(BUILD)/src && pkg="$(PWD)/$(BUILD)/pkg" ../build.bash | ||||||
| 	fakeroot dpkg-deb --build $(BUILD)/pkg $(BUILD)/$(DEB) | 	fakeroot dpkg-deb --build $(BUILD)/pkg $(BUILD)/$(DEB) | ||||||
| 
 | 
 | ||||||
| ### Run things inside Docker
 | ### Manipulate artifacts inside Docker
 | ||||||
| 
 | 
 | ||||||
| VOLUME_MOUNT ?= $(PWD) | VOLUME_MOUNT ?= $(PWD) | ||||||
| 
 | 
 | ||||||
|  | @ -57,7 +57,7 @@ shell: | ||||||
| ifeq ($(I),admin) | 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) | 	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 | 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 | endif | ||||||
| 
 | 
 | ||||||
| .PHONY: install | .PHONY: install | ||||||
|  | @ -66,7 +66,42 @@ install: | ||||||
| 	[[ -z "$$(ls -A /var/lib/apt/lists)" ]] && sudo apt update | 	[[ -z "$$(ls -A /var/lib/apt/lists)" ]] && sudo apt update | ||||||
| 	sudo apt reinstall -y ./$(BUILD)/$(DEB) | 	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 | .PHONY: pull | ||||||
| pull: | pull: | ||||||
|  | @ -80,7 +115,7 @@ download: | ||||||
| 	mkdir -p $(BUILD) | 	mkdir -p $(BUILD) | ||||||
| 	aws s3 cp $(S3_DEB) $(BUILD)/$(DEB) | 	aws s3 cp $(S3_DEB) $(BUILD)/$(DEB) | ||||||
| 
 | 
 | ||||||
| ### Publish things to registries
 | ### Publish artifacts to registries
 | ||||||
| 
 | 
 | ||||||
| .PHONY: push | .PHONY: push | ||||||
| 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 | RUN /tmp/install.bash | ||||||
| 
 | 
 | ||||||
| WORKDIR /src | WORKDIR /src | ||||||
| COPY docker/admin/pid1.bash /usr/local/sbin/ | COPY docker/shared/my_init docker/admin/pid1.bash /usr/local/sbin/ | ||||||
| ENTRYPOINT ["/usr/local/sbin/pid1.bash"] | ENTRYPOINT ["/usr/local/sbin/my_init", "/usr/local/sbin/pid1.bash"] | ||||||
| CMD ["bash"] | CMD ["bash"] | ||||||
|  |  | ||||||
|  | @ -27,7 +27,7 @@ deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) sta | ||||||
| EOF | EOF | ||||||
| 
 | 
 | ||||||
| apt-get update | 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 | wget https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip -O awscli.zip | ||||||
| unzip awscli.zip | unzip awscli.zip | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ | ||||||
| set -euo pipefail | set -euo pipefail | ||||||
| 
 | 
 | ||||||
| groupadd -g "$(stat -c %g "$PWD")" -o -p '!' -r riju | 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 -- touch /home/riju/.sudo_as_admin_successful | ||||||
| runuser -u riju -- ln -sT /var/riju/.aws /home/riju/.aws | 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 | RUN /tmp/install.bash | ||||||
| 
 | 
 | ||||||
| WORKDIR /src | WORKDIR /src | ||||||
| COPY docker/packaging/pid1.bash /usr/local/sbin/ | COPY docker/shared/my_init docker/packaging/pid1.bash /usr/local/sbin/ | ||||||
| ENTRYPOINT ["/usr/local/sbin/pid1.bash"] | ENTRYPOINT ["/usr/local/sbin/my_init", "/usr/local/sbin/pid1.bash"] | ||||||
| CMD ["bash"] | CMD ["bash"] | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ | ||||||
| set -euo pipefail | set -euo pipefail | ||||||
| 
 | 
 | ||||||
| groupadd -g "$(stat -c %g "$PWD")" -o -p '!' -r riju | 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 touch /home/riju/.sudo_as_admin_successful | ||||||
| runuser -u riju -- yarn install | runuser -u riju -- yarn install | ||||||
|  |  | ||||||
|  | @ -4,6 +4,8 @@ COPY docker/runtime/install.bash /tmp/ | ||||||
| RUN /tmp/install.bash | RUN /tmp/install.bash | ||||||
| 
 | 
 | ||||||
| WORKDIR /src | WORKDIR /src | ||||||
| COPY docker/runtime/pid1.bash /usr/local/sbin/ | COPY docker/shared/my_init docker/runtime/pid1.bash /usr/local/sbin/ | ||||||
| ENTRYPOINT ["/usr/local/sbin/pid1.bash"] | ENTRYPOINT ["/usr/local/sbin/my_init", "/usr/local/sbin/pid1.bash"] | ||||||
| CMD ["bash"] | CMD ["bash"] | ||||||
|  | EXPOSE 6119 | ||||||
|  | EXPOSE 6120 | ||||||
|  |  | ||||||
|  | @ -2,6 +2,12 @@ | ||||||
| 
 | 
 | ||||||
| set -euxo pipefail | 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 | export DEBIAN_FRONTEND=noninteractive | ||||||
| 
 | 
 | ||||||
| apt-get update | 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)" | libicu="$(grep-aptavail -wF Package 'libicu[0-9]+' -s Package -n | head -n1)" | ||||||
| 
 | 
 | ||||||
| apt-get update | 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/* | 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 | %sudo ALL=(ALL:ALL) NOPASSWD: ALL | ||||||
| EOF | EOF | ||||||
| 
 | 
 | ||||||
|  | popd | ||||||
|  | 
 | ||||||
| rm "$0" | rm "$0" | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ | ||||||
| set -euo pipefail | set -euo pipefail | ||||||
| 
 | 
 | ||||||
| groupadd -g "$(stat -c %g "$PWD")" -o -p '!' -r riju | 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 touch /home/riju/.sudo_as_admin_successful | ||||||
| runuser -u riju -- yarn install | 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, |   "private": true, | ||||||
|   "type": "module", |   "type": "module", | ||||||
|   "dependencies": { |   "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", |     "commander": "^6.2.1", | ||||||
|  |     "css-loader": "^5.0.1", | ||||||
|  |     "ejs": "^3.1.5", | ||||||
|     "express": "^4.17.1", |     "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" |     "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