├── .dockerignore ├── backend └── src │ ├── global.d.ts │ ├── config.ts │ ├── sandbox.ts │ ├── users.ts │ ├── lsp-repl.ts │ ├── server.ts │ ├── util.ts │ ├── api.ts │ └── test-runner.ts ├── flag.png ├── .gitignore ├── scripts ├── certbot-pre.bash ├── certbot-post.bash ├── docker.bash ├── riju.service ├── setup.bash ├── docker-install-phase0.bash ├── pid1.bash ├── install-scripts.bash ├── docker-install-phase8.bash ├── deploy.bash ├── riju-serve.bash ├── compile-system.bash ├── docker-install-phase2.bash ├── deploy-phase2.py ├── deploy-phase1.py ├── docker-install-phase3b.bash ├── docker-install-phase3d.bash ├── docker-install-phase3a.bash ├── docker-install-phase3c.bash ├── docker-install-phase1.bash ├── docker-install-phase5.bash ├── docker-install-phase6.bash ├── docker-install-phase7.bash ├── docker-install-phase4.bash └── my_init ├── tsconfig-webpack.json ├── tsconfig.json ├── .circleci └── config.yml ├── frontend ├── styles │ ├── index.css │ └── app.css ├── pages │ ├── index.ejs │ └── app.ejs └── src │ └── app.ts ├── Makefile ├── LICENSE.md ├── webpack.config.js ├── Dockerfile.dev ├── package.json ├── Dockerfile.prod ├── system └── src │ └── riju-system-privileged.c └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | .gitignore -------------------------------------------------------------------------------- /backend/src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "historic-readline"; 2 | -------------------------------------------------------------------------------- /flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martimarkov/riju/HEAD/flag.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .git 2 | *.log 3 | .log 4 | .lsp-repl-history 5 | node_modules 6 | out 7 | tests 8 | tests-* 9 | -------------------------------------------------------------------------------- /scripts/certbot-pre.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -o pipefail 5 | 6 | systemctl stop riju 7 | -------------------------------------------------------------------------------- /scripts/certbot-post.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -o pipefail 5 | 6 | systemctl start riju 7 | -------------------------------------------------------------------------------- /backend/src/config.ts: -------------------------------------------------------------------------------- 1 | import * as process from "process"; 2 | 3 | export const PRIVILEGED = process.env.RIJU_PRIVILEGED ? true : false; 4 | -------------------------------------------------------------------------------- /scripts/docker.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -o pipefail 5 | 6 | if [[ "$OSTYPE" != darwin* ]] && [[ "$EUID" != 0 ]]; then 7 | exec sudo -E docker "$@" 8 | else 9 | exec docker "$@" 10 | fi 11 | -------------------------------------------------------------------------------- /tsconfig-webpack.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./frontend/out", 4 | "rootDir": "./frontend/src", 5 | "target": "ES3" 6 | }, 7 | "extends": "./tsconfig.json", 8 | "include": ["frontend/src"] 9 | } 10 | -------------------------------------------------------------------------------- /scripts/riju.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Riju online coding sandbox 3 | Requires=docker.service 4 | After=docker.service 5 | 6 | [Service] 7 | Type=exec 8 | ExecStart=riju-serve 9 | Restart=always 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /scripts/setup.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -o pipefail 5 | 6 | mkdir -p /tmp/riju 7 | if [[ -x system/out/riju-system-privileged ]]; then 8 | system/out/riju-system-privileged teardown "*" "*" || true 9 | fi 10 | chmod a=x,u=rwx /tmp/riju 11 | -------------------------------------------------------------------------------- /scripts/docker-install-phase0.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -o pipefail 5 | set -x 6 | 7 | export DEBIAN_FRONTEND=noninteractive 8 | apt-get update 9 | apt-get dist-upgrade -y 10 | (yes || true) | unminimize 11 | rm -rf /var/lib/apt/lists/* 12 | 13 | rm "$0" 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "downlevelIteration": true, 4 | "outDir": "./backend/out", 5 | "resolveJsonModule": true, 6 | "rootDir": "./backend/src", 7 | "sourceMap": true, 8 | "strict": true, 9 | "target": "ES5" 10 | }, 11 | "include": ["backend/src"] 12 | } 13 | -------------------------------------------------------------------------------- /scripts/pid1.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -o pipefail 5 | 6 | export LANG=C.UTF-8 7 | export LC_ALL=C.UTF-8 8 | export SHELL="$(which bash)" 9 | 10 | export HOST=0.0.0.0 11 | export RIJU_PRIVILEGED=yes 12 | 13 | if [[ -d /home/docker/src ]]; then 14 | cd /home/docker/src 15 | fi 16 | 17 | exec "$@" 18 | -------------------------------------------------------------------------------- /scripts/install-scripts.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -o pipefail 5 | 6 | cp scripts/riju.service /etc/systemd/system/riju.service 7 | cp scripts/riju-serve.bash /usr/local/bin/riju-serve 8 | cp scripts/certbot-pre.bash /etc/letsencrypt/renewal-hooks/pre/riju 9 | cp scripts/certbot-post.bash /etc/letsencrypt/renewal-hooks/post/riju 10 | cp scripts/deploy-phase1.py /usr/local/bin/riju-deploy 11 | 12 | systemctl daemon-reload 13 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build_and_deploy: 4 | docker: 5 | - image: alpine 6 | steps: 7 | - checkout 8 | - run: apk add --no-cache --no-progress bash openssh 9 | - run: scripts/deploy.bash 10 | workflows: 11 | version: 2 12 | ci: 13 | jobs: 14 | - build_and_deploy: 15 | filters: 16 | branches: 17 | only: master 18 | tags: 19 | ignore: /.*/ 20 | -------------------------------------------------------------------------------- /scripts/docker-install-phase8.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -o pipefail 5 | set -x 6 | 7 | uid="$1" 8 | 9 | rm -rf /tmp/hsperfdata_root 10 | 11 | if [[ -n "$uid" ]] && (( "$uid" != 0 )); then 12 | useradd --uid="$uid" --password "!" --create-home --groups sudo docker 13 | else 14 | useradd --password "!" --create-home --groups sudo docker 15 | fi 16 | 17 | tee /etc/sudoers.d/90-passwordless >/dev/null <<"EOF" 18 | %sudo ALL=(ALL:ALL) NOPASSWD: ALL 19 | EOF 20 | 21 | touch /home/docker/.zshrc 22 | chown docker:docker /home/docker/.zshrc 23 | 24 | rm "$0" 25 | -------------------------------------------------------------------------------- /scripts/deploy.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -o pipefail 5 | 6 | tmpdir="$(mktemp -d)" 7 | keyfile="${tmpdir}/id" 8 | 9 | if [[ -n "$DEPLOY_KEY" ]]; then 10 | printf '%s\n' "$DEPLOY_KEY" | base64 -d > "$keyfile" 11 | elif [[ -f "$HOME/.ssh/id_rsa_riju_deploy" ]]; then 12 | cp "$HOME/.ssh/id_rsa_riju_deploy" "$keyfile" 13 | else 14 | echo 'deploy.bash: you must set $DEPLOY_KEY' >&2 15 | exit 1 16 | fi 17 | 18 | chmod go-rw "$keyfile" 19 | ssh -o IdentitiesOnly=yes \ 20 | -o StrictHostKeyChecking=no \ 21 | -o UserKnownHostsFile=/dev/null \ 22 | -o LogLevel=QUIET \ 23 | -i "${keyfile}" deploy@riju.codes 24 | -------------------------------------------------------------------------------- /scripts/riju-serve.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -o pipefail 5 | 6 | TLS=1 7 | TLS_PRIVATE_KEY="$(base64 /etc/letsencrypt/live/riju.codes/privkey.pem)" 8 | TLS_CERTIFICATE="$(base64 /etc/letsencrypt/live/riju.codes/fullchain.pem)" 9 | ANALYTICS=1 10 | 11 | # Do this separately so that errors in command substitution will crash 12 | # the script. 13 | export TLS TLS_PRIVATE_KEY TLS_CERTIFICATE ANALYTICS 14 | 15 | if [[ -t 1 ]]; then 16 | it=-it 17 | else 18 | it= 19 | fi 20 | 21 | docker run ${it} -e TLS -e TLS_PRIVATE_KEY -e TLS_CERTIFICATE -e ANALYTICS \ 22 | --rm -p 0.0.0.0:80:6119 -p 0.0.0.0:443:6120 -h riju riju:prod 23 | -------------------------------------------------------------------------------- /frontend/styles/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | font-family: sans-serif; 6 | text-align: center; 7 | padding-bottom: 20px; 8 | } 9 | 10 | .grid { 11 | display: flex; 12 | flex-wrap: wrap; 13 | justify-content: center; 14 | margin-top: 20px; 15 | } 16 | 17 | div.language { 18 | width: 140px; 19 | height: 60px; 20 | border: solid; 21 | margin: 5px; 22 | padding: 5px; 23 | display: flex; 24 | justify-content: center; 25 | align-items: center; 26 | text-align: center; 27 | font-size: 18px; 28 | } 29 | 30 | a.language { 31 | text-decoration: none; 32 | color: black; 33 | } 34 | -------------------------------------------------------------------------------- /scripts/compile-system.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -o pipefail 5 | 6 | if [[ ! -d system/src ]]; then 7 | echo "compile-system.bash: no system/src directory" >&2 8 | exit 1 9 | fi 10 | 11 | function verbosely { 12 | echo "$@" 13 | "$@" 14 | } 15 | 16 | mkdir -p system/out 17 | rm -f system/out/* 18 | for src in system/src/*.c; do 19 | out="${src/src/out}" 20 | out="${out/.c}" 21 | verbosely clang -Wall -Wextra -Werror -std=c11 "${src}" -o "${out}" 22 | if [[ "${out}" == *-privileged && -n "${RIJU_PRIVILEGED}" ]]; then 23 | sudo chown root:docker "${out}" 24 | sudo chmod a=,g=rx,u=rwxs "${out}" 25 | fi 26 | done 27 | -------------------------------------------------------------------------------- /frontend/styles/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | height: 100vh; 4 | } 5 | 6 | #app { 7 | height: 100%; 8 | } 9 | 10 | .column { 11 | width: 50%; 12 | height: 100%; 13 | float: left; 14 | } 15 | 16 | #editor { 17 | overflow: hidden; 18 | } 19 | 20 | #terminal { 21 | background: black; 22 | } 23 | 24 | #runButton { 25 | position: absolute; 26 | top: 25px; 27 | right: calc(50% + 25px); 28 | } 29 | 30 | #formatButton { 31 | position: absolute; 32 | bottom: 25px; 33 | right: calc(50% + 25px); 34 | visibility: hidden; 35 | } 36 | 37 | #formatButton.visible { 38 | visibility: visible; 39 | } 40 | 41 | #backButton { 42 | position: absolute; 43 | left: 25px; 44 | bottom: 25px; 45 | } 46 | -------------------------------------------------------------------------------- /scripts/docker-install-phase2.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -o pipefail 5 | set -x 6 | 7 | packages=" 8 | 9 | # Needed for project infrastructure 10 | bash 11 | dctrl-tools 12 | git 13 | make 14 | nodejs 15 | python3-pip 16 | yarn 17 | 18 | # Handy utilities 19 | apt-file 20 | bbe 21 | bsdmainutils 22 | curl 23 | emacs-nox 24 | git 25 | httpie 26 | htop 27 | jq 28 | lsof 29 | make 30 | man-db 31 | moreutils 32 | nano 33 | ncdu 34 | iputils-ping 35 | ripgrep 36 | strace 37 | sudo 38 | tmux 39 | trash-cli 40 | tree 41 | vim 42 | wget 43 | 44 | " 45 | 46 | export DEBIAN_FRONTEND=noninteractive 47 | apt-get update 48 | apt-get install -y $(grep -v "^#" <<< "$packages") 49 | rm -rf /var/lib/apt/lists/* 50 | 51 | rm "$0" 52 | -------------------------------------------------------------------------------- /scripts/deploy-phase2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import errno 5 | import os 6 | import re 7 | import signal 8 | import subprocess 9 | import sys 10 | import tempfile 11 | import time 12 | 13 | subprocess.run(["docker", "pull", "ubuntu:rolling"], check=True) 14 | subprocess.run(["docker", "system", "prune", "-f"], check=True) 15 | subprocess.run(["make", "image-prod"], check=True) 16 | existing_containers = subprocess.run( 17 | ["docker", "ps", "-q"], check=True, stdout=subprocess.PIPE 18 | ).stdout.splitlines() 19 | subprocess.run(["scripts/install-scripts.bash"], check=True) 20 | if existing_containers: 21 | subprocess.run(["docker", "kill", *existing_containers], check=True) 22 | subprocess.run(["systemctl", "enable", "riju"], check=True) 23 | subprocess.run(["systemctl", "restart", "riju"], check=True) 24 | 25 | print("==> Successfully deployed Riju! <==", file=sys.stderr) 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | UID := $(shell id -u) 2 | 3 | .PHONY: help 4 | help: ## Show this message 5 | @echo "usage:" >&2 6 | @grep -h "[#]# " $(MAKEFILE_LIST) | \ 7 | sed 's/^/ make /' | \ 8 | sed 's/:[^#]*[#]# /|/' | \ 9 | column -t -s'|' >&2 10 | 11 | .PHONY: image-dev 12 | image-dev: ## Build Docker image for development 13 | scripts/docker.bash build . -f Dockerfile.dev -t riju --build-arg "UID=$(UID)" 14 | 15 | .PHONY: image-prod 16 | image-prod: ## Build Docker image for production 17 | scripts/docker.bash build . -f Dockerfile.prod -t riju:prod --build-arg "UID=$(UID)" 18 | 19 | .PHONY: docker 20 | docker: image-dev docker-nobuild ## Run shell with source code and deps inside Docker 21 | 22 | .PHONY: docker 23 | docker-nobuild: ## Same as 'make docker', but don't rebuild image 24 | scripts/docker.bash run -it --rm -v "$(PWD):/home/docker/src" -p 6119:6119 -p 6120:6120 -h riju riju bash 25 | 26 | .PHONY: deploy 27 | deploy: ## Deploy current master from GitHub to production 28 | scripts/deploy.bash 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2020 Radon Rosborough 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /scripts/deploy-phase1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import errno 5 | import os 6 | import re 7 | import signal 8 | import subprocess 9 | import sys 10 | import tempfile 11 | import time 12 | 13 | result = subprocess.run(["pgrep", "-x", "riju-deploy"], stdout=subprocess.PIPE) 14 | assert result.returncode in {0, 1} 15 | for pid in result.stdout.decode().splitlines(): 16 | print(f"Found existing process {pid}, trying to kill ...", file=sys.stderr) 17 | pid = int(pid) 18 | os.kill(pid, signal.SIGTERM) 19 | while True: 20 | time.sleep(0.01) 21 | try: 22 | os.kill(pid, 0) 23 | except OSError as e: 24 | if e.errno == errno.ESRCH: 25 | break 26 | 27 | with tempfile.TemporaryDirectory() as tmpdir: 28 | os.chdir(tmpdir) 29 | subprocess.run( 30 | [ 31 | "git", 32 | "clone", 33 | "https://github.com/raxod502/riju.git", 34 | "--single-branch", 35 | "--depth=1", 36 | "--no-tags", 37 | ], 38 | check=True, 39 | ) 40 | os.chdir("riju") 41 | subprocess.run(["scripts/deploy-phase2.py"], check=True) 42 | -------------------------------------------------------------------------------- /scripts/docker-install-phase3b.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -o pipefail 5 | set -x 6 | 7 | export DEBIAN_FRONTEND=noninteractive 8 | apt-get update 9 | 10 | lua_ver="$(grep-aptavail -XF Provides lua -s Version -n | sort -Vr | head -n1)" 11 | lua_name="$(grep-aptavail -XF Provides lua -a -XF Version "${lua_ver}" -s Package -n | head -n1)" 12 | 13 | packages=" 14 | 15 | # Elixir 16 | elixir 17 | 18 | # Elvish 19 | elvish 20 | 21 | # Emacs Lisp 22 | emacs-nox 23 | 24 | # Erlang 25 | erlang 26 | libodbc1 # workaround bug in APT 27 | rebar 28 | 29 | # F# 30 | fsharp 31 | 32 | # Fish 33 | fish 34 | 35 | # FORTRAN 36 | flang 37 | 38 | # Forth 39 | gforth 40 | 41 | # Go 42 | golang 43 | 44 | # Groovy 45 | groovy 46 | 47 | # Hack 48 | hhvm 49 | 50 | # Haskell 51 | cabal-install 52 | ghc 53 | 54 | # Haxe 55 | haxe 56 | 57 | # INTERCAL 58 | intercal 59 | 60 | # Java 61 | clang-format 62 | default-jdk 63 | 64 | # Julia 65 | julia 66 | 67 | # Ksh 68 | ksh 69 | 70 | # LLVM 71 | llvm 72 | 73 | # LOLCODE 74 | cmake 75 | 76 | # Lua 77 | ${lua_name} 78 | 79 | " 80 | 81 | apt-get install -y $(sed 's/#.*//' <<< "$packages") 82 | rm -rf /var/lib/apt/lists/* 83 | 84 | rm "$0" 85 | -------------------------------------------------------------------------------- /frontend/pages/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Riju 6 | 7 | 8 | 9 |

Riju: fast online playground for every programming language

10 | Pick your favorite language to get started: 11 |
12 | <% for (const [id, {name}] of Object.entries(langs).sort( 13 | ([id1, {name: name1}], [id2, {name: name2}]) => name1.toLowerCase().localeCompare(name2.toLowerCase()))) { %> 14 | class="language"> 15 |
16 | <%= name %> 17 |
18 |
19 | <% } %> 20 |
21 |

22 | 23 | Created by 24 | Radon Rosborough. 25 | Check out the project 26 | on GitHub. 27 | 28 |

29 | <% if (analyticsEnabled) { %> 30 | 31 | <% } %> 32 | 33 | 34 | -------------------------------------------------------------------------------- /frontend/pages/app.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= config.name %> - Riju 6 | 12 | 13 | 14 | 15 |
16 |
17 |
18 | 19 | 20 | Switch to a different language 21 |
22 | 25 | 26 | <% if (analyticsEnabled) { %> 27 | 28 | <% } %> 29 | 30 | 31 | -------------------------------------------------------------------------------- /scripts/docker-install-phase3d.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -o pipefail 5 | set -x 6 | 7 | export DEBIAN_FRONTEND=noninteractive 8 | apt-get update 9 | 10 | lua_ver="$(grep-aptavail -XF Provides lua -s Version -n | sort -Vr | head -n1)" 11 | liblua_name="$(grep-aptavail -eF Package "liblua[0-9.]+-dev" -a -XF Version "${lua_ver}" -s Package -n | head -n1)" 12 | 13 | packages=" 14 | 15 | # Scala 16 | scala 17 | 18 | # Scheme 19 | mit-scheme 20 | 21 | # Sed 22 | sed 23 | 24 | # Sh 25 | posh 26 | 27 | # Smalltalk 28 | gnu-smalltalk 29 | 30 | # SNOBOL 31 | m4 32 | 33 | # SQLite 34 | sqlite 35 | 36 | # Standard ML 37 | rlwrap 38 | smlnj 39 | 40 | # Swift 41 | libpython2.7 42 | 43 | # Tcl 44 | tcl 45 | 46 | # Tcsh 47 | tcsh 48 | 49 | # TeX 50 | ${liblua_name} 51 | luarocks 52 | texlive-binaries 53 | 54 | # Unlambda 55 | unlambda 56 | 57 | # Vimscript 58 | vim 59 | 60 | # Visual Basic 61 | mono-vbnc 62 | 63 | # Wolfram Language 64 | python3.7 65 | 66 | # x86 67 | clang 68 | 69 | # YAML 70 | jq 71 | 72 | # Zot 73 | qt5-qmake 74 | qtscript5-dev 75 | 76 | # Zsh 77 | zsh 78 | 79 | " 80 | 81 | apt-get install -y $(grep -v "^#" <<< "$packages") 82 | rm -rf /var/lib/apt/lists/* 83 | 84 | rm "$0" 85 | -------------------------------------------------------------------------------- /scripts/docker-install-phase3a.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -o pipefail 5 | set -x 6 | 7 | export DEBIAN_FRONTEND=noninteractive 8 | apt-get update 9 | 10 | ceylon="$(grep-aptavail -F Package ceylon -s Package -n | sort -rV | head -n1)" 11 | 12 | packages=" 13 | 14 | # Ada 15 | gnat 16 | 17 | # Algol 18 | algol68g 19 | 20 | # APL 21 | libtinfo5 22 | 23 | # ARM 24 | gcc-arm-linux-gnueabihf 25 | qemu-user-static 26 | 27 | # AsciiDoc 28 | asciidoc 29 | 30 | # ATS 31 | ats2-lang 32 | 33 | # Awk 34 | mawk 35 | 36 | # BASIC 37 | bwbasic 38 | 39 | # Bash 40 | bash 41 | 42 | # Battlestar 43 | golang 44 | yasm 45 | 46 | # BrainF 47 | beef 48 | 49 | # C/C++ 50 | clang 51 | clang-format 52 | clangd 53 | 54 | # C# 55 | clang-format 56 | mono-mcs 57 | 58 | # Ceylon 59 | ${ceylon} 60 | openjdk-8-jdk-headless 61 | 62 | # Clojure 63 | clojure 64 | 65 | # Cmd 66 | wine 67 | wine32 68 | 69 | # COBOL 70 | gnucobol 71 | 72 | # Common Lisp 73 | rlwrap 74 | sbcl 75 | 76 | # Crystal 77 | crystal 78 | 79 | # Dart 80 | dart 81 | 82 | # Dhall 83 | dhall 84 | 85 | # Dylan 86 | libunwind-dev 87 | 88 | " 89 | 90 | apt-get install -y $(grep -v "^#" <<< "$packages") 91 | rm -rf /var/lib/apt/lists/* 92 | 93 | rm "$0" 94 | -------------------------------------------------------------------------------- /scripts/docker-install-phase3c.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -o pipefail 5 | set -x 6 | 7 | packages=" 8 | 9 | # MariaDB 10 | libtinfo5 11 | 12 | # MIPS 13 | gcc-mips64-linux-gnuabi64 14 | qemu-user-static 15 | 16 | # MongoDB 17 | mongodb 18 | 19 | # MUMPS 20 | fis-gtm 21 | 22 | # MySQL 23 | mysql-server 24 | 25 | # Nim 26 | nim 27 | 28 | # Node.js 29 | nodejs 30 | yarn 31 | 32 | # Objective-C 33 | gcc 34 | gnustep-devel 35 | 36 | # OCaml 37 | ocaml 38 | opam 39 | 40 | # Octave 41 | octave 42 | 43 | # Pascal 44 | fpc 45 | 46 | # Perl 47 | perl 48 | perlconsole 49 | 50 | # PHP 51 | php 52 | 53 | # PostgreSQL 54 | postgresql 55 | postgresql-client 56 | 57 | # Prolog 58 | swi-prolog 59 | 60 | # PureScript 61 | libtinfo5 62 | 63 | # Python 64 | python3 65 | python3-pip 66 | python3-venv 67 | 68 | # R 69 | r-base 70 | 71 | # Racket 72 | racket 73 | 74 | # Rapira 75 | clang 76 | 77 | # Redis 78 | redis 79 | 80 | # RISC-V 81 | gcc-riscv64-linux-gnu 82 | qemu-user-static 83 | 84 | # Ruby 85 | ruby 86 | ruby-dev 87 | 88 | " 89 | 90 | export DEBIAN_FRONTEND=noninteractive 91 | apt-get update 92 | apt-get install -y $(grep -v "^#" <<< "$packages") 93 | rm -rf /var/lib/apt/lists/* 94 | 95 | rm /etc/mysql/mysql.conf.d/mysqld.cnf 96 | 97 | rm "$0" 98 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin"); 4 | 5 | function isProduction(argv) { 6 | return !argv.development; 7 | } 8 | 9 | module.exports = (_, argv) => ({ 10 | devtool: isProduction(argv) ? undefined : "source-map", 11 | entry: "./frontend/src/app.ts", 12 | mode: isProduction(argv) ? "production" : "development", 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.css$/i, 17 | use: ["style-loader", "css-loader"], 18 | }, 19 | { 20 | test: /\.tsx?$/i, 21 | loader: "ts-loader", 22 | options: { 23 | configFile: "tsconfig-webpack.json", 24 | }, 25 | exclude: /node_modules/, 26 | }, 27 | { 28 | test: /\.ttf$/, 29 | use: ["file-loader"], 30 | }, 31 | { 32 | test: /\.js$/, 33 | use: { 34 | loader: "babel-loader", 35 | options: { 36 | presets: ["@babel/preset-env"], 37 | }, 38 | }, 39 | include: /vscode-jsonrpc/, 40 | }, 41 | ], 42 | }, 43 | node: { 44 | net: "mock", 45 | }, 46 | output: { 47 | path: path.resolve(__dirname, "frontend/out"), 48 | publicPath: "/js/", 49 | filename: "app.js", 50 | }, 51 | performance: { 52 | hints: false, 53 | }, 54 | plugins: [new MonacoWebpackPlugin()], 55 | resolve: { 56 | alias: { 57 | vscode: require.resolve("monaco-languageclient/lib/vscode-compatibility"), 58 | }, 59 | }, 60 | }); 61 | -------------------------------------------------------------------------------- /backend/src/sandbox.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from "child_process"; 2 | import * as fs from "fs"; 3 | 4 | import { v4 as getUUID } from "uuid"; 5 | 6 | import { langs } from "./langs"; 7 | import { MIN_UID, MAX_UID, borrowUser, ignoreUsers } from "./users"; 8 | import { 9 | privilegedSetup, 10 | privilegedSpawn, 11 | privilegedTeardown, 12 | run, 13 | } from "./util"; 14 | 15 | function die(msg: any) { 16 | console.error(msg); 17 | process.exit(1); 18 | } 19 | 20 | function log(msg: any) { 21 | console.log(msg); 22 | } 23 | 24 | async function main() { 25 | const dirs = await new Promise((resolve, reject) => 26 | fs.readdir("/tmp/riju", (err, dirs) => (err ? reject(err) : resolve(dirs))) 27 | ); 28 | const uids = ( 29 | await Promise.all( 30 | dirs.map( 31 | (dir) => 32 | new Promise((resolve, reject) => 33 | fs.stat(`/tmp/riju/${dir}`, (err, stat) => 34 | err ? reject(err) : resolve(stat.uid) 35 | ) 36 | ) 37 | ) 38 | ) 39 | ).filter((uid) => uid >= MIN_UID && uid < MAX_UID); 40 | await ignoreUsers(uids, log); 41 | const uuid = getUUID(); 42 | const { uid, returnUID } = await borrowUser(log); 43 | await run(privilegedSetup({ uid, uuid }), log); 44 | const args = privilegedSpawn({ uid, uuid }, ["bash"]); 45 | const proc = spawn(args[0], args.slice(1), { 46 | stdio: "inherit", 47 | }); 48 | await new Promise((resolve, reject) => { 49 | proc.on("error", reject); 50 | proc.on("close", resolve); 51 | }); 52 | await run(privilegedTeardown({ uid, uuid }), log); 53 | await returnUID(); 54 | } 55 | 56 | main().catch(die); 57 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM ubuntu:rolling 2 | 3 | ARG UID 4 | 5 | COPY scripts/my_init /usr/bin/my_init 6 | 7 | COPY scripts/docker-install-phase0.bash /tmp/ 8 | RUN bash -c "time /tmp/docker-install-phase0.bash" 9 | 10 | COPY scripts/docker-install-phase1.bash /tmp/ 11 | RUN bash -c "time /tmp/docker-install-phase1.bash" 12 | 13 | COPY scripts/docker-install-phase2.bash /tmp/ 14 | RUN bash -c "time /tmp/docker-install-phase2.bash" 15 | 16 | COPY scripts/docker-install-phase3a.bash /tmp/ 17 | RUN bash -c "time /tmp/docker-install-phase3a.bash" 18 | 19 | COPY scripts/docker-install-phase3b.bash /tmp/ 20 | RUN bash -c "time /tmp/docker-install-phase3b.bash" 21 | 22 | COPY scripts/docker-install-phase3c.bash /tmp/ 23 | RUN bash -c "time /tmp/docker-install-phase3c.bash" 24 | 25 | COPY scripts/docker-install-phase3d.bash /tmp/ 26 | RUN bash -c "time /tmp/docker-install-phase3d.bash" 27 | 28 | COPY scripts/docker-install-phase4.bash /tmp/ 29 | RUN bash -c "time /tmp/docker-install-phase4.bash" 30 | 31 | COPY scripts/docker-install-phase5.bash /tmp/ 32 | RUN bash -c "time /tmp/docker-install-phase5.bash" 33 | 34 | COPY scripts/docker-install-phase6.bash /tmp/ 35 | RUN bash -c "time /tmp/docker-install-phase6.bash" 36 | 37 | COPY scripts/docker-install-phase7.bash /tmp/ 38 | RUN bash -c "time /tmp/docker-install-phase7.bash" 39 | 40 | COPY scripts/docker-install-phase8.bash /tmp/ 41 | RUN bash -c "time /tmp/docker-install-phase8.bash '$UID'" 42 | 43 | USER docker 44 | WORKDIR /home/docker 45 | RUN chmod go-rwx /home/docker 46 | EXPOSE 6119 47 | EXPOSE 6120 48 | 49 | ENTRYPOINT ["/usr/bin/my_init", "/usr/local/bin/pid1.bash"] 50 | COPY scripts/pid1.bash /usr/local/bin/ 51 | CMD ["bash"] 52 | -------------------------------------------------------------------------------- /scripts/docker-install-phase1.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -o pipefail 5 | set -x 6 | pushd /tmp >/dev/null 7 | 8 | dpkg --add-architecture i386 9 | 10 | export DEBIAN_FRONTEND=noninteractive 11 | apt-get update 12 | apt-get install -y apt-transport-https curl gnupg lsb-release software-properties-common wget 13 | rm -rf /var/lib/apt/lists/* 14 | 15 | ubuntu_ver="$(lsb_release -rs)" 16 | ubuntu_name="$(lsb_release -cs)" 17 | 18 | curl -sSL https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - 19 | curl -sSL https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - 20 | curl -sSL https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - 21 | curl -sSL https://downloads.ceylon-lang.org/apt/ceylon-debian-repo.gpg.key | apt-key add - 22 | curl -sSL https://keybase.io/crystal/pgp_keys.asc | apt-key add - 23 | apt-key adv --keyserver keyserver.ubuntu.com --recv-keys E298A3A825C0D65DFD57CBB651716619E084DAB9 24 | apt-key adv --keyserver keyserver.ubuntu.com --recv-keys B4112585D386EB94 25 | 26 | wget -nv "https://packages.microsoft.com/config/ubuntu/${ubuntu_ver}/packages-microsoft-prod.deb" 27 | dpkg -i packages-microsoft-prod.deb 28 | rm packages-microsoft-prod.deb 29 | 30 | nodesource="$(curl -sS https://deb.nodesource.com/setup_current.x | grep NODEREPO= | grep -Eo 'node_[0-9]+\.x' | head -n1)" 31 | cran="$(curl -sS https://cran.r-project.org/bin/linux/ubuntu/ | grep '' | grep focal | grep -Eo 'cran[0-9]+' | head -n1)" 32 | 33 | tee -a /etc/apt/sources.list.d/custom.list >/dev/null </dev/null 46 | rm "$0" 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "riju", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "private": true, 6 | "dependencies": { 7 | "@babel/core": "^7.10.3", 8 | "@babel/preset-env": "^7.10.3", 9 | "@types/app-root-path": "^1.2.4", 10 | "@types/async-lock": "^1.1.2", 11 | "@types/express": "^4.17.6", 12 | "@types/express-ws": "^3.0.0", 13 | "@types/lodash": "^4.14.155", 14 | "@types/mkdirp": "^1.0.1", 15 | "@types/node-cleanup": "^2.1.1", 16 | "@types/parse-passwd": "^1.0.0", 17 | "@types/rimraf": "^3.0.0", 18 | "@types/shell-quote": "^1.7.0", 19 | "@types/tmp": "^0.2.0", 20 | "@types/uuid": "^8.0.0", 21 | "app-root-path": "^3.0.0", 22 | "async-lock": "^1.2.4", 23 | "babel-loader": "^8.1.0", 24 | "css-loader": "^3.5.3", 25 | "ejs": "^3.1.3", 26 | "express": "^4.17.1", 27 | "express-ws": "^4.0.0", 28 | "file-loader": "^6.0.0", 29 | "historic-readline": "^1.0.8", 30 | "lodash": "^4.17.15", 31 | "moment": "^2.27.0", 32 | "monaco-editor": "^0.20.0", 33 | "monaco-editor-webpack-plugin": "^1.9.0", 34 | "monaco-languageclient": "^0.13.0", 35 | "node-pty": "^0.9.0", 36 | "npm-run-all": "^4.1.5", 37 | "p-queue": "^6.6.0", 38 | "parse-passwd": "^1.0.0", 39 | "rimraf": "^3.0.2", 40 | "shell-quote": "^1.7.2", 41 | "style-loader": "^1.2.1", 42 | "ts-loader": "^7.0.5", 43 | "typescript": "^3.9.5", 44 | "uuid": "^8.1.0", 45 | "vscode": "^1.1.37", 46 | "vscode-jsonrpc": "^5.0.1", 47 | "webpack": "^4.43.0", 48 | "webpack-cli": "^3.3.11", 49 | "xterm": "^4.6.0", 50 | "xterm-addon-fit": "^0.4.0" 51 | }, 52 | "scripts": { 53 | "backend": "tsc", 54 | "backend-dev": "TSC_NONPOLLING_WATCHER=1 tsc --watch --preserveWatchOutput", 55 | "frontend": "webpack --production", 56 | "frontend-dev": "webpack --development --watch", 57 | "server": "scripts/setup.bash && node backend/out/server.js", 58 | "server-dev": "watchexec --no-vcs-ignore -w backend/out -r 'scripts/setup.bash && node backend/out/server.js'", 59 | "system": "scripts/compile-system.bash", 60 | "system-dev": "watchexec --no-vcs-ignore -w system/src -n scripts/compile-system.bash", 61 | "build": "run-s backend frontend system", 62 | "dev": "run-p backend-dev frontend-dev system-dev server-dev", 63 | "lsp-repl": "node backend/out/lsp-repl.js", 64 | "sandbox": "node backend/out/sandbox.js", 65 | "test": "bash -c 'scripts/setup.bash && time node backend/out/test-runner.js \"$@\"' --" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Dockerfile.prod: -------------------------------------------------------------------------------- 1 | FROM ubuntu:rolling 2 | 3 | # This is just here so we can reuse the Docker cache between dev and 4 | # prod, it's not actually read by anything. 5 | ARG UID 6 | 7 | COPY scripts/my_init /usr/bin/my_init 8 | 9 | COPY scripts/docker-install-phase0.bash /tmp/ 10 | RUN bash -c "time /tmp/docker-install-phase0.bash" 11 | 12 | COPY scripts/docker-install-phase1.bash /tmp/ 13 | RUN bash -c "time /tmp/docker-install-phase1.bash" 14 | 15 | COPY scripts/docker-install-phase2.bash /tmp/ 16 | RUN bash -c "time /tmp/docker-install-phase2.bash" 17 | 18 | COPY scripts/docker-install-phase3a.bash /tmp/ 19 | RUN bash -c "time /tmp/docker-install-phase3a.bash" 20 | 21 | COPY scripts/docker-install-phase3b.bash /tmp/ 22 | RUN bash -c "time /tmp/docker-install-phase3b.bash" 23 | 24 | COPY scripts/docker-install-phase3c.bash /tmp/ 25 | RUN bash -c "time /tmp/docker-install-phase3c.bash" 26 | 27 | COPY scripts/docker-install-phase3d.bash /tmp/ 28 | RUN bash -c "time /tmp/docker-install-phase3d.bash" 29 | 30 | COPY scripts/docker-install-phase4.bash /tmp/ 31 | RUN bash -c "time /tmp/docker-install-phase4.bash" 32 | 33 | COPY scripts/docker-install-phase5.bash /tmp/ 34 | RUN bash -c "time /tmp/docker-install-phase5.bash" 35 | 36 | COPY scripts/docker-install-phase6.bash /tmp/ 37 | RUN bash -c "time /tmp/docker-install-phase6.bash" 38 | 39 | COPY scripts/docker-install-phase7.bash /tmp/ 40 | RUN bash -c "time /tmp/docker-install-phase7.bash" 41 | 42 | COPY scripts/docker-install-phase8.bash /tmp/ 43 | RUN bash -c "time /tmp/docker-install-phase8.bash '$UID'" 44 | 45 | USER docker 46 | WORKDIR /home/docker 47 | RUN chmod go-rwx /home/docker 48 | EXPOSE 6119 49 | EXPOSE 6120 50 | 51 | ENTRYPOINT ["/usr/bin/my_init", "/usr/local/bin/pid1.bash"] 52 | COPY scripts/pid1.bash /usr/local/bin/ 53 | CMD ["yarn", "run", "server"] 54 | 55 | RUN mkdir /tmp/riju /tmp/riju/scripts 56 | COPY --chown=docker:docker package.json yarn.lock /tmp/riju/ 57 | RUN bash -c "cd /tmp/riju && time yarn install" 58 | COPY --chown=docker:docker webpack.config.js tsconfig.json tsconfig-webpack.json /tmp/riju/ 59 | COPY --chown=docker:docker frontend /tmp/riju/frontend 60 | RUN bash -c "cd /tmp/riju && time yarn run frontend" 61 | COPY --chown=docker:docker backend /tmp/riju/backend 62 | RUN bash -c "cd /tmp/riju && time yarn run backend" 63 | COPY --chown=docker:docker scripts/compile-system.bash /tmp/riju/scripts 64 | COPY --chown=docker:docker system /tmp/riju/system 65 | RUN bash -c "cd /tmp/riju && time RIJU_PRIVILEGED=1 yarn run system" 66 | COPY --chown=docker:docker . /home/docker/src 67 | RUN sudo cp -a /tmp/riju/* /home/docker/src/ && rm -rf /tmp/riju 68 | 69 | WORKDIR /home/docker/src 70 | RUN sudo deluser docker sudo 71 | RUN RIJU_PRIVILEGED=1 CONCURRENCY=1 TIMEOUT_FACTOR=5 LANG=C.UTF-8 LC_ALL=C.UTF-8 yarn test 72 | -------------------------------------------------------------------------------- /scripts/docker-install-phase5.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -o pipefail 5 | set -x 6 | 7 | # Package manager - Julia 8 | mkdir /opt/julia 9 | export JULIA_DEPOT_PATH=/opt/julia 10 | 11 | # Package manager - Node.js 12 | npm config set unsafe-perm true 13 | PERL_MM_USE_DEFAULT=1 cpan App::cpanminus 14 | rm -rf /tmp/cpan_install_*.txt 15 | 16 | # Package manager - OCaml 17 | export OPAMROOT=/opt/opam 18 | export OPAMROOTISOK=1 19 | opam init -n --disable-sandboxing 20 | 21 | # Shared 22 | npm install -g prettier 23 | 24 | # Bash 25 | npm install -g bash-language-server 26 | 27 | # Befunge 28 | npm install -g befunge93 prompt-sync 29 | 30 | # Chef 31 | cpanm -n Acme::Chef 32 | 33 | # ClojureScript 34 | npm install -g lumo-cljs 35 | 36 | # CoffeeScript 37 | npm install -g coffeescript 38 | 39 | # D 40 | dub fetch --version='~master' dfmt 41 | dub run dfmt -- --version 42 | mv "$HOME/.dub/packages/dfmt-master/dfmt/bin/dfmt" /usr/local/bin/ 43 | 44 | # Dogescript 45 | npm install -g dogescript 46 | 47 | # Elm 48 | npm install -g @kachkaev/run-elm 49 | npm install -g @elm-tooling/elm-language-server 50 | 51 | # FORTRAN 52 | pip3 install fortran-language-server 53 | 54 | # Hy 55 | pip3 install hy 56 | 57 | # Julia 58 | julia -e 'using Pkg; Pkg.add("LanguageServer")' 59 | 60 | # Less 61 | npm install -g less 62 | 63 | # LiveScript 64 | npm install -g livescript 65 | 66 | # OCaml 67 | opam install -y ocamlformat 68 | opam pin add -y ocaml-lsp-server https://github.com/ocaml/ocaml-lsp.git 69 | ln -s /opt/opam/default/bin/ocamlformat /usr/local/bin/ocamlformat 70 | ln -s /opt/opam/default/bin/ocamllsp /usr/local/bin/ocamllsp 71 | ln -s /opt/opam/default/bin/refmt /usr/local/bin/refmt 72 | 73 | # Perl 74 | cpanm -n Devel::REPL 75 | cpanm -n Perl::Tidy 76 | 77 | # PHP 78 | npm install -g intelephense 79 | 80 | # Pikachu 81 | pip3 install pikalang 82 | 83 | # Pug 84 | npm install -g pug-cli 85 | 86 | # PureScript 87 | npm install -g purescript spago 88 | 89 | # Python 90 | pip3 install black 91 | 92 | # ReasonML 93 | npm install -g bs-platform 94 | opam install -y reason 95 | 96 | # Ruby 97 | gem install rufo 98 | gem install solargraph 99 | 100 | # Rust 101 | rustup component add rls rust-analysis rust-src 102 | 103 | # Sass/SCSS 104 | npm install -g sass 105 | 106 | # Shakespeare 107 | pip3 install shakespearelang 108 | 109 | # TeX 110 | luarocks install digestif 111 | 112 | # TypeScript 113 | npm install -g ts-node typescript 114 | 115 | # Vim 116 | npm install -g vim-language-server 117 | 118 | # Whitespace 119 | pip3 install whitespace 120 | 121 | # Wolfram Language 122 | python3.7 -m pip install mathics 123 | 124 | rm -rf /root/.cache /root/.config /root/.cpan /root/.cpanm /root/.dub /root/.gem /root/.npm /root/.npmrc 125 | rm -f /tmp/core-js-banners 126 | 127 | rm "$0" 128 | -------------------------------------------------------------------------------- /backend/src/users.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from "child_process"; 2 | import * as fs from "fs"; 3 | import * as os from "os"; 4 | 5 | import * as AsyncLock from "async-lock"; 6 | import * as _ from "lodash"; 7 | import * as parsePasswd from "parse-passwd"; 8 | 9 | import { PRIVILEGED } from "./config"; 10 | import { privilegedUseradd, run } from "./util"; 11 | 12 | // Keep in sync with system/src/riju-system-privileged.c 13 | export const MIN_UID = 2000; 14 | export const MAX_UID = 65000; 15 | 16 | const CUR_UID = os.userInfo().uid; 17 | 18 | let availIds: number[] | null = null; 19 | let nextId: number | null = null; 20 | let lock = new AsyncLock(); 21 | 22 | async function readExistingUsers(log: (msg: string) => void) { 23 | availIds = parsePasswd( 24 | await new Promise((resolve: (result: string) => void, reject) => 25 | fs.readFile("/etc/passwd", "utf-8", (err, data) => { 26 | if (err) { 27 | reject(err); 28 | } else { 29 | resolve(data); 30 | } 31 | }) 32 | ) 33 | ) 34 | .filter(({ username }) => username.startsWith("riju")) 35 | .map(({ uid }) => parseInt(uid)) 36 | .filter((uid) => !isNaN(uid) && uid >= MIN_UID && uid < MAX_UID) 37 | .reverse(); 38 | nextId = (_.max(availIds) || MIN_UID - 1) + 1; 39 | log(`Found ${availIds.length} existing users, next is riju${nextId}`); 40 | } 41 | 42 | async function createUser(log: (msg: string) => void): Promise { 43 | if (nextId! >= MAX_UID) { 44 | throw new Error("too many users"); 45 | } 46 | const uid = nextId!; 47 | await run(privilegedUseradd(uid), log); 48 | log(`Created new user with ID ${uid}`); 49 | nextId! += 1; 50 | return uid; 51 | } 52 | 53 | export async function ignoreUsers(uids: number[], log: (msg: string) => void) { 54 | await lock.acquire("key", async () => { 55 | if (availIds === null || nextId === null) { 56 | await readExistingUsers(log); 57 | } 58 | const uidSet = new Set(uids); 59 | if (uidSet.size > 0) { 60 | const plural = uidSet.size !== 1 ? "s" : ""; 61 | log( 62 | `Ignoring user${plural} from open session${plural}: ${Array.from(uidSet) 63 | .sort() 64 | .map((uid) => `riju${uid}`) 65 | .join(", ")}` 66 | ); 67 | } 68 | availIds = availIds!.filter((uid) => !uidSet.has(uid)); 69 | }); 70 | } 71 | 72 | export async function borrowUser(log: (msg: string) => void) { 73 | if (!PRIVILEGED) { 74 | return { uid: CUR_UID, returnUID: async () => {} }; 75 | } else { 76 | return await lock.acquire("key", async () => { 77 | if (availIds === null || nextId === null) { 78 | await readExistingUsers(log); 79 | } 80 | let uid: number; 81 | if (availIds!.length > 0) { 82 | uid = availIds!.pop()!; 83 | } else { 84 | uid = await createUser(log); 85 | } 86 | return { 87 | uid, 88 | returnUID: async () => { 89 | await lock.acquire("key", () => { 90 | availIds!.push(uid); 91 | }); 92 | }, 93 | }; 94 | }); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /backend/src/lsp-repl.ts: -------------------------------------------------------------------------------- 1 | import * as child_process from "child_process"; 2 | import * as process from "process"; 3 | import * as nodeReadline from "readline"; 4 | 5 | import * as appRoot from "app-root-path"; 6 | import * as readline from "historic-readline"; 7 | import { quote } from "shell-quote"; 8 | import * as rpc from "vscode-jsonrpc"; 9 | 10 | import { langs } from "./langs"; 11 | 12 | const args = process.argv.slice(2); 13 | 14 | function printUsage() { 15 | console.log(`usage: yarn lsp-repl (LANG | CMDLINE...)`); 16 | } 17 | 18 | if (args.length === 0) { 19 | printUsage(); 20 | process.exit(1); 21 | } 22 | 23 | if (["-h", "-help", "--help", "help"].includes(args[0])) { 24 | printUsage(); 25 | process.exit(0); 26 | } 27 | 28 | let cmdline; 29 | if (args.length === 1 && langs[args[0]] && langs[args[0]].lsp) { 30 | cmdline = ["bash", "-c", langs[args[0]].lsp!.start]; 31 | } else { 32 | cmdline = args; 33 | } 34 | 35 | console.error(quote(cmdline)); 36 | const proc = child_process.spawn(cmdline[0], cmdline.slice(1)); 37 | 38 | proc.stderr.on("data", (data) => process.stderr.write(data)); 39 | proc.on("close", (code, signal) => { 40 | if (code) { 41 | console.error(`Language server exited with code ${code}`); 42 | process.exit(code); 43 | } else { 44 | console.error(`Language server exited due to signal ${signal}`); 45 | process.exit(1); 46 | } 47 | }); 48 | proc.on("error", (err) => { 49 | console.error(`Failed to start language server: ${err}`); 50 | process.exit(1); 51 | }); 52 | 53 | const reader = new rpc.StreamMessageReader(proc.stdout); 54 | const writer = new rpc.StreamMessageWriter(proc.stdin); 55 | 56 | reader.listen((data) => { 57 | console.log("<<< " + JSON.stringify(data) + "\n"); 58 | }); 59 | 60 | // https://stackoverflow.com/a/10608048/3538165 61 | function fixStdoutFor(cli: any) { 62 | var oldStdout = process.stdout; 63 | var newStdout = Object.create(oldStdout); 64 | newStdout.write = function () { 65 | cli.output.write("\x1b[2K\r"); 66 | var result = oldStdout.write.apply( 67 | this, 68 | (Array.prototype.slice as any).call(arguments) 69 | ); 70 | cli._refreshLine(); 71 | return result; 72 | }; 73 | (process as any).__defineGetter__("stdout", function () { 74 | return newStdout; 75 | }); 76 | } 77 | 78 | readline.createInterface({ 79 | input: process.stdin, 80 | output: process.stdout, 81 | path: appRoot.resolve(".lsp-repl-history"), 82 | next: (cli: nodeReadline.Interface) => { 83 | fixStdoutFor(cli); 84 | cli.setPrompt(">>> "); 85 | cli.on("line", (line: string) => { 86 | if (line) { 87 | let data; 88 | try { 89 | data = JSON.parse(line); 90 | } catch (err) { 91 | console.error(`Invalid JSON: ${err}`); 92 | cli.prompt(); 93 | return; 94 | } 95 | console.log(); 96 | writer.write(data); 97 | } 98 | cli.prompt(); 99 | }); 100 | cli.on("SIGINT", () => { 101 | console.error("^C"); 102 | cli.write("", { ctrl: true, name: "u" }); 103 | cli.prompt(); 104 | }); 105 | cli.on("close", () => { 106 | console.error(); 107 | process.exit(0); 108 | }); 109 | console.log(); 110 | cli.prompt(); 111 | }, 112 | }); 113 | -------------------------------------------------------------------------------- /scripts/docker-install-phase6.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -o pipefail 5 | set -x 6 | pushd /tmp >/dev/null 7 | 8 | # Battlestar 9 | git clone https://github.com/xyproto/battlestar.git 10 | pushd battlestar >/dev/null 11 | make 12 | mv cmd/battlestarc/battlestarc /usr/local/bin/ 13 | mv scripts/bts.sh /usr/local/bin/bts 14 | popd >/dev/null 15 | rm -rf battlestar 16 | 17 | # Beatnik 18 | git clone https://github.com/catseye/Beatnik.git 19 | sed -i 's#env python#env python2#' Beatnik/script/beatnik.py 20 | mv Beatnik/script/beatnik.py /usr/local/bin/beatnik 21 | rm -rf Beatnik 22 | 23 | # Binary Lambda Calculus 24 | wget -nv https://www.ioccc.org/2012/tromp/tromp.c 25 | clang tromp.c -Wno-everything -DInt=long -DX=8 -DA=500000 -o /usr/local/bin/tromp 26 | rm tromp.c 27 | 28 | # Cat 29 | git clone https://github.com/cdiggins/cat-language.git /opt/cat 30 | pushd /opt/cat >/dev/null 31 | npm install 32 | popd >/dev/null 33 | 34 | # Clean 35 | wget -nv "$(curl -sSL https://clean.cs.ru.nl/Download_Clean | grep linux/clean | grep -F 64.tar.gz | grep -Eo "https://[^>]+\.tar\.gz")" 36 | mkdir /opt/clean 37 | tar -xf clean*_64.tar.gz -C /opt/clean --strip-components=1 38 | pushd /opt/clean >/dev/null 39 | make 40 | popd >/dev/null 41 | ln -s /opt/clean/bin/clm /usr/local/bin/ 42 | rm clean*_64.tar.gz 43 | 44 | sleep 2 45 | find /opt/clean -name '*.o' -exec touch '{}' ';' 46 | 47 | # Erlang 48 | git clone https://github.com/erlang-ls/erlang_ls.git 49 | pushd erlang_ls >/dev/null 50 | make 51 | mv _build/default/bin/erlang_ls /usr/local/bin/erlang_ls 52 | popd >/dev/null 53 | rm -rf erlang_ls 54 | 55 | # Hexagony 56 | git clone https://github.com/m-ender/hexagony.git /opt/hexagony 57 | 58 | # Kalyn 59 | git clone https://github.com/raxod502/kalyn.git 60 | pushd kalyn >/dev/null 61 | stack build kalyn 62 | mv "$(stack exec which kalyn)" /usr/local/bin/kalyn 63 | mkdir /opt/kalyn 64 | mv src-kalyn/Stdlib src-kalyn/Stdlib.kalyn /opt/kalyn/ 65 | popd >/dev/null 66 | rm -rf kalyn 67 | 68 | # LOLCODE 69 | git clone https://github.com/justinmeza/lci.git 70 | pushd lci >/dev/null 71 | python3 install.py --prefix=/usr 72 | popd >/dev/null 73 | rm -rf lci 74 | 75 | # Malbolge 76 | git clone https://github.com/bipinu/malbolge.git 77 | clang malbolge/malbolge.c -o /usr/local/bin/malbolge 78 | rm -rf malbolge 79 | 80 | # Rapira 81 | git clone https://github.com/freeduke33/rerap2.git 82 | pushd rerap2 >/dev/null 83 | make 84 | mv rapira /usr/local/bin/rapira 85 | popd >/dev/null 86 | rm -rf rerap2 87 | 88 | # Qalb 89 | git clone https://github.com/nasser/---.git qalb 90 | pushd qalb >/dev/null 91 | mkdir -p /opt/qalb 92 | mv public/qlb/*.js /opt/qalb/ 93 | popd >/dev/null 94 | rm -rf qalb 95 | 96 | # Snobol 97 | file="$(curl -sSL ftp://ftp.snobol4.org/snobol/ | grep -Eo 'snobol4-.*\.tar\.gz' | sort -rV | head -n1)" 98 | wget -nv "ftp://ftp.snobol4.org/snobol/${file}" 99 | tar -xf snobol4-*.tar.gz 100 | rm snobol4-*.tar.gz 101 | pushd snobol4-* >/dev/null 102 | make || true 103 | mv snobol4 /usr/local/bin/snobol4 104 | popd >/dev/null 105 | rm -rf snobol4-* 106 | 107 | # Thue 108 | wget -nv "$(curl -sSL https://catseye.tc/distribution/Thue_distribution | grep -Eo 'https://catseye.tc/distfiles/thue-[^"]+\.zip' | head -n1)" 109 | unzip thue-*.zip 110 | rm thue-*.zip 111 | pushd thue-* >/dev/null 112 | ./build.sh 113 | mv bin/thue /usr/local/bin/thue 114 | popd >/dev/null 115 | rm -rf thue-* 116 | 117 | # Zot 118 | git clone https://github.com/manyoso/zot.git 119 | pushd zot >/dev/null 120 | ./build.sh 121 | mv build/bin/zot /usr/local/bin/zot 122 | popd >/dev/null 123 | rm -rf zot 124 | 125 | popd >/dev/null 126 | rm "$0" 127 | -------------------------------------------------------------------------------- /backend/src/server.ts: -------------------------------------------------------------------------------- 1 | import * as http from "http"; 2 | import * as https from "https"; 3 | 4 | import * as appRoot from "app-root-path"; 5 | import * as express from "express"; 6 | import { Request } from "express"; 7 | import * as ws from "express-ws"; 8 | import * as _ from "lodash"; 9 | 10 | import * as api from "./api"; 11 | import { langs } from "./langs"; 12 | 13 | const host = process.env.HOST || "localhost"; 14 | const port = parseInt(process.env.PORT || "") || 6119; 15 | const tlsPort = parseInt(process.env.TLS_PORT || "") || 6120; 16 | const useTLS = process.env.TLS ? true : false; 17 | const analyticsEnabled = process.env.ANALYTICS ? true : false; 18 | 19 | const app = express(); 20 | 21 | app.set("query parser", (qs: string) => new URLSearchParams(qs)); 22 | app.set("view engine", "ejs"); 23 | 24 | function getQueryParams(req: Request): URLSearchParams { 25 | // This is safe because we set the query parser for Express to 26 | // return URLSearchParams objects. 27 | return (req.query as unknown) as URLSearchParams; 28 | } 29 | 30 | app.get("/", (_, res) => { 31 | res.render(appRoot.path + "/frontend/pages/index", { 32 | langs, 33 | analyticsEnabled, 34 | }); 35 | }); 36 | for (const [lang, { aliases }] of Object.entries(langs)) { 37 | if (aliases) { 38 | for (const alias of aliases) { 39 | app.get(`/${_.escapeRegExp(alias)}`, (_, res) => { 40 | res.redirect(301, `/${lang}`); 41 | }); 42 | } 43 | } 44 | } 45 | app.get("/:lang", (req, res) => { 46 | const lang = req.params.lang; 47 | const lowered = lang.toLowerCase(); 48 | if (lowered !== lang) { 49 | res.redirect(301, `/${lowered}`); 50 | } else if (langs[lang]) { 51 | res.render(appRoot.path + "/frontend/pages/app", { 52 | config: { id: lang, ...langs[lang] }, 53 | analyticsEnabled, 54 | }); 55 | } else { 56 | res.send(`No such language: ${lang}`); 57 | } 58 | }); 59 | app.use("/css", express.static(appRoot.path + "/frontend/styles")); 60 | app.use("/js", express.static(appRoot.path + "/frontend/out")); 61 | 62 | function addWebsocket( 63 | baseApp: express.Express, 64 | httpsServer: https.Server | undefined 65 | ) { 66 | const app = ws(baseApp, httpsServer).app; 67 | app.ws("/api/v1/ws", (ws, req) => { 68 | const lang = getQueryParams(req).get("lang"); 69 | if (!lang) { 70 | ws.send( 71 | JSON.stringify({ 72 | event: "error", 73 | errorMessage: "No language specified", 74 | }) 75 | ); 76 | ws.close(); 77 | } else if (!langs[lang]) { 78 | ws.send( 79 | JSON.stringify({ 80 | event: "error", 81 | errorMessage: `No such language: ${lang}`, 82 | }) 83 | ); 84 | ws.close(); 85 | } else { 86 | new api.Session(ws, lang, console.log).setup(); 87 | } 88 | }); 89 | return app; 90 | } 91 | 92 | if (useTLS) { 93 | const httpsServer = https.createServer( 94 | { 95 | key: Buffer.from(process.env.TLS_PRIVATE_KEY || "", "base64").toString( 96 | "ascii" 97 | ), 98 | cert: Buffer.from(process.env.TLS_CERTIFICATE || "", "base64").toString( 99 | "ascii" 100 | ), 101 | }, 102 | app 103 | ); 104 | addWebsocket(app, httpsServer); 105 | httpsServer.listen(tlsPort, host, () => 106 | console.log(`Listening on https://${host}:${tlsPort}`) 107 | ); 108 | const server = http 109 | .createServer((req, res) => { 110 | res.writeHead(301, { 111 | Location: "https://" + req.headers["host"] + req.url, 112 | }); 113 | res.end(); 114 | }) 115 | .listen(port, host, () => 116 | console.log(`Listening on http://${host}:${port}`) 117 | ); 118 | } else { 119 | addWebsocket(app, undefined); 120 | const server = app.listen(port, host, () => 121 | console.log(`Listening on http://${host}:${port}`) 122 | ); 123 | } 124 | -------------------------------------------------------------------------------- /backend/src/util.ts: -------------------------------------------------------------------------------- 1 | import { SpawnOptions, spawn, spawnSync } from "child_process"; 2 | import * as os from "os"; 3 | import * as process from "process"; 4 | 5 | import * as appRoot from "app-root-path"; 6 | import { quote } from "shell-quote"; 7 | 8 | import { MIN_UID, MAX_UID } from "./users"; 9 | 10 | export interface Options extends SpawnOptions { 11 | input?: string; 12 | check?: boolean; 13 | } 14 | 15 | export interface Context { 16 | uid: number; 17 | uuid: string; 18 | } 19 | 20 | export const rijuSystemPrivileged = appRoot.resolve( 21 | "system/out/riju-system-privileged" 22 | ); 23 | 24 | const rubyVersion = (() => { 25 | try { 26 | return spawnSync("ruby", ["-e", "puts RUBY_VERSION"]) 27 | .stdout.toString() 28 | .trim(); 29 | } catch (err) { 30 | return null; 31 | } 32 | })(); 33 | 34 | function getEnv({ uid, uuid }: Context) { 35 | const cwd = `/tmp/riju/${uuid}`; 36 | const path = [ 37 | rubyVersion && `${cwd}/.gem/ruby/${rubyVersion}/bin`, 38 | `${cwd}/.local/bin`, 39 | `${cwd}/node_modules/.bin`, 40 | `/usr/local/sbin`, 41 | `/usr/local/bin`, 42 | `/usr/sbin`, 43 | `/usr/bin`, 44 | `/bin`, 45 | ].filter((x) => x); 46 | const username = 47 | uid >= MIN_UID && uid < MAX_UID ? `riju${uid}` : os.userInfo().username; 48 | return { 49 | HOME: cwd, 50 | HOSTNAME: "riju", 51 | LANG: process.env.LANG || "", 52 | LC_ALL: process.env.LC_ALL || "", 53 | LOGNAME: username, 54 | PATH: path.join(":"), 55 | PWD: cwd, 56 | SHELL: "/usr/bin/bash", 57 | TERM: "xterm-256color", 58 | TMPDIR: `${cwd}`, 59 | USER: username, 60 | USERNAME: username, 61 | }; 62 | } 63 | 64 | function getEnvString(ctx: Context) { 65 | return Object.entries(getEnv(ctx)) 66 | .map(([key, val]) => `${key}=${quote([val])}`) 67 | .join(" "); 68 | } 69 | 70 | export async function run( 71 | args: string[], 72 | log: (msg: string) => void, 73 | options?: Options 74 | ) { 75 | options = options || {}; 76 | const input = options.input; 77 | const check = options.check === undefined ? true : options.check; 78 | delete options.input; 79 | delete options.check; 80 | const proc = spawn(args[0], args.slice(1), options); 81 | if (typeof input === "string") { 82 | proc.stdin!.end(input); 83 | } 84 | let output = ""; 85 | proc.stdout!.on("data", (data: Buffer) => { 86 | output += `${data}`; 87 | }); 88 | proc.stderr!.on("data", (data: Buffer) => { 89 | output += `${data}`; 90 | }); 91 | return await new Promise((resolve, reject) => { 92 | proc.on("error", reject); 93 | proc.on("close", (code: number, signal: string) => { 94 | output = output.trim(); 95 | if (output) { 96 | log(`Output from ${args[0]}:\n` + output); 97 | } 98 | if (code === 0 || !check) { 99 | resolve(code); 100 | } else { 101 | reject(`command ${args[0]} failed with error code ${signal || code}`); 102 | } 103 | }); 104 | }); 105 | } 106 | 107 | export function privilegedUseradd(uid: number) { 108 | return [rijuSystemPrivileged, "useradd", `${uid}`]; 109 | } 110 | 111 | export function privilegedSetup({ uid, uuid }: Context) { 112 | return [rijuSystemPrivileged, "setup", `${uid}`, uuid]; 113 | } 114 | 115 | export function privilegedSpawn(ctx: Context, args: string[]) { 116 | const { uid, uuid } = ctx; 117 | return [ 118 | rijuSystemPrivileged, 119 | "spawn", 120 | `${uid}`, 121 | uuid, 122 | "sh", 123 | "-c", 124 | `exec env -i ${getEnvString(ctx)} "$@"`, 125 | "--", 126 | ].concat(args); 127 | } 128 | 129 | export function privilegedTeardown({ uid, uuid }: Context) { 130 | return [rijuSystemPrivileged, "teardown", `${uid}`, uuid]; 131 | } 132 | 133 | export function bash(cmdline: string) { 134 | if (!cmdline.match(/[;|&(){}=]/)) { 135 | // Reduce number of subshells we generate, if we're just running a 136 | // single command (no shell logic). 137 | cmdline = "exec " + cmdline; 138 | } 139 | return ["bash", "-c", cmdline]; 140 | } 141 | -------------------------------------------------------------------------------- /system/src/riju-system-privileged.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | // Keep in sync with backend/src/users.ts 12 | const int MIN_UID = 2000; 13 | const int MAX_UID = 65000; 14 | 15 | int privileged; 16 | 17 | void __attribute__ ((noreturn)) die(char *msg) 18 | { 19 | fprintf(stderr, "%s\n", msg); 20 | exit(1); 21 | } 22 | 23 | void die_with_usage() 24 | { 25 | die("usage:\n" 26 | " riju-system-privileged useradd UID\n" 27 | " riju-system-privileged setup UID UUID\n" 28 | " riju-system-privileged spawn UID UUID CMDLINE...\n" 29 | " riju-system-privileged teardown UID UUID"); 30 | } 31 | 32 | int parseUID(char *str) 33 | { 34 | if (!privileged) 35 | return -1; 36 | char *endptr; 37 | long uid = strtol(str, &endptr, 10); 38 | if (!*str || *endptr) 39 | die("uid must be an integer"); 40 | if (uid < MIN_UID || uid >= MAX_UID) 41 | die("uid is out of range"); 42 | return uid; 43 | } 44 | 45 | char *parseUUID(char *uuid) 46 | { 47 | if (!*uuid) 48 | die("illegal uuid"); 49 | for (char *ptr = uuid; *ptr; ++ptr) 50 | if (!((*ptr >= 'a' && *ptr <= 'z') || (*ptr >= '0' && *ptr <= '9') || *ptr == '-')) 51 | die("illegal uuid"); 52 | return uuid; 53 | } 54 | 55 | void useradd(int uid) 56 | { 57 | if (!privileged) 58 | die("useradd not allowed without root privileges"); 59 | char *cmdline; 60 | if (asprintf(&cmdline, "groupadd -g %1$d riju%1$d", uid) < 0) 61 | die("asprintf failed"); 62 | int status = system(cmdline); 63 | if (status != 0) 64 | die("groupadd failed"); 65 | if (asprintf(&cmdline, "useradd -M -N -l -r -u %1$d -g %1$d -p '!' -s /usr/bin/bash riju%1$d", uid) < 0) 66 | die("asprintf failed"); 67 | status = system(cmdline); 68 | if (status != 0) 69 | die("useradd failed"); 70 | } 71 | 72 | void spawn(int uid, char *uuid, char **cmdline) 73 | { 74 | char *cwd; 75 | if (asprintf(&cwd, "/tmp/riju/%s", uuid) < 0) 76 | die("asprintf failed"); 77 | if (chdir(cwd) < 0) 78 | die("chdir failed"); 79 | if (privileged) { 80 | if (setgid(uid) < 0) 81 | die("setgid failed"); 82 | if (setgroups(0, NULL) < 0) 83 | die("setgroups failed"); 84 | if (setuid(uid) < 0) 85 | die("setuid failed"); 86 | } 87 | umask(077); 88 | execvp(cmdline[0], cmdline); 89 | die("execvp failed"); 90 | } 91 | 92 | void setup(int uid, char *uuid) 93 | { 94 | char *cmdline; 95 | if (asprintf(&cmdline, privileged 96 | ? "install -d -o riju%1$d -g riju%1$d -m 700 /tmp/riju/%2$s" 97 | : "install -d -m 700 /tmp/riju/%2$s", uid, uuid) < 0) 98 | die("asprintf failed"); 99 | int status = system(cmdline); 100 | if (status != 0) 101 | die("install failed"); 102 | } 103 | 104 | void teardown(int uid, char *uuid) 105 | { 106 | char *cmdline; 107 | int status; 108 | char *users; 109 | if (uid >= MIN_UID && uid < MAX_UID) { 110 | if (asprintf(&users, "%d", uid) < 0) 111 | die("asprintf failed"); 112 | } else { 113 | cmdline = "getent passwd | grep -Eo '^riju[0-9]{4}' | paste -s -d, - | tr -d '\n'"; 114 | FILE *fp = popen(cmdline, "r"); 115 | if (fp == NULL) 116 | die("popen failed"); 117 | static char buf[(MAX_UID - MIN_UID) * 9]; 118 | if (fgets(buf, sizeof(buf), fp) == NULL) { 119 | if (feof(fp)) 120 | users = NULL; 121 | else { 122 | die("fgets failed"); 123 | } 124 | } else 125 | users = buf; 126 | } 127 | if (users != NULL) { 128 | if (asprintf(&cmdline, "while pkill -9 --uid %1$s; do sleep 0.01; done", users) < 0) 129 | die("asprintf failed"); 130 | status = system(cmdline); 131 | if (status != 0 && status != 256) 132 | die("pkill failed"); 133 | } 134 | if (asprintf(&cmdline, "rm -rf /tmp/riju/%s", uuid) < 0) 135 | die("asprintf failed"); 136 | status = system(cmdline); 137 | if (status != 0) 138 | die("rm failed"); 139 | } 140 | 141 | int main(int argc, char **argv) 142 | { 143 | int code = setuid(0); 144 | if (code != 0 && code != -EPERM) 145 | die("setuid failed"); 146 | privileged = code == 0; 147 | if (argc < 2) 148 | die_with_usage(); 149 | if (!strcmp(argv[1], "useradd")) { 150 | if (argc != 3) 151 | die_with_usage(); 152 | useradd(parseUID(argv[2])); 153 | return 0; 154 | } 155 | if (!strcmp(argv[1], "spawn")) { 156 | if (argc < 5) 157 | die_with_usage(); 158 | spawn(parseUID(argv[2]), parseUUID(argv[3]), &argv[4]); 159 | return 0; 160 | } 161 | if (!strcmp(argv[1], "setup")) { 162 | if (argc != 4) 163 | die_with_usage(); 164 | int uid = parseUID(argv[2]); 165 | char *uuid = parseUUID(argv[3]); 166 | setup(uid, uuid); 167 | return 0; 168 | } 169 | if (!strcmp(argv[1], "teardown")) { 170 | if (argc != 4) 171 | die_with_usage(); 172 | int uid = strcmp(argv[2], "*") ? parseUID(argv[2]) : -1; 173 | char *uuid = strcmp(argv[3], "*") ? parseUUID(argv[3]) : "*"; 174 | teardown(uid, uuid); 175 | return 0; 176 | } 177 | die_with_usage(); 178 | } 179 | -------------------------------------------------------------------------------- /scripts/docker-install-phase7.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -o pipefail 5 | set -x 6 | pushd /tmp >/dev/null 7 | useradd -m -N -l -r -p '!' build 8 | 9 | # Cmd 10 | sudo -u build wine cmd < /dev/null 11 | mkdir -p /opt/cmd/home-template 12 | mv /home/build/.wine /opt/cmd/home-template/ 13 | chmod -R a=u,go-w /opt/cmd/home-template 14 | 15 | # Dylan 16 | pushd /opt/dylan 17 | make-dylan-app main 18 | mv main project-template 19 | pushd project-template >/dev/null 20 | cat <<"EOF" > main.dylan 21 | Module: main 22 | 23 | define function main 24 | (name :: , arguments :: ) 25 | format-out("Hello, world!\n"); 26 | exit-application(0); 27 | end function main; 28 | 29 | main(application-name(), application-arguments()); 30 | EOF 31 | dylan-compiler -build main.lid 32 | rm main.dylan 33 | popd >/dev/null 34 | popd >/dev/null 35 | 36 | # Elm 37 | mkdir -p /opt/elm 38 | mkdir elm-project 39 | pushd elm-project >/dev/null 40 | (yes || true) | elm init 41 | cat elm.json | jq '."source-directories" = ["."]' > /opt/elm/elm.json 42 | popd >/dev/null 43 | rm -rf elm-project 44 | 45 | # PureScript 46 | mkdir project-template 47 | pushd project-template >/dev/null 48 | spago init -C 49 | rm -rf .gitignore test 50 | sed -i 's#, "test/\*\*/\*\.purs"##' spago.dhall 51 | cat <<"EOF" > src/Main.spago 52 | import Prelude 53 | 54 | import Effect (Effect) 55 | 56 | main :: Effect Unit 57 | main = pure unit 58 | EOF 59 | spago build 60 | spago repl < /dev/null 61 | rm -rf src 62 | popd >/dev/null 63 | mkdir /opt/purescript 64 | mv project-template /opt/purescript/ 65 | 66 | # ReasonML 67 | mkdir -p /opt/reasonml/project-template 68 | pushd /opt/reasonml/project-template >/dev/null 69 | bsb -init . 70 | cat bsconfig.json | jq '.name = "riju-project"' | sponge bsconfig.json 71 | yarn install 72 | popd >/dev/null 73 | 74 | # Unison 75 | mkdir -p /opt/unison/project-template 76 | pushd /opt/unison/project-template >/dev/null 77 | unison -codebase . init 78 | LESS="+q" unison -codebase . <<< 'pull https://github.com/unisonweb/base:.trunk .base' 79 | popd >/dev/null 80 | 81 | # Befunge 82 | tee /usr/local/bin/befunge-repl >/dev/null <<"EOF" 83 | #!/usr/bin/env -S NODE_PATH=/usr/lib/node_modules node 84 | const fs = require("fs"); 85 | 86 | const Befunge = require("befunge93"); 87 | const prompt = require("prompt-sync")(); 88 | 89 | const befunge = new Befunge(); 90 | befunge.onInput = prompt; 91 | befunge.onOutput = (output) => { 92 | if (typeof output === "string") { 93 | process.stdout.write(output); 94 | } else { 95 | process.stdout.write(output + " "); 96 | } 97 | }; 98 | 99 | const args = process.argv.slice(2); 100 | if (args.length !== 1) { 101 | console.error("usage: befunge-repl FILE"); 102 | process.exit(1); 103 | } 104 | 105 | befunge.run(fs.readFileSync(args[0], { encoding: "utf-8" })).catch((err) => { 106 | console.error(err); 107 | process.exit(1); 108 | }); 109 | EOF 110 | chmod +x /usr/local/bin/befunge-repl 111 | 112 | # Binary Lambda Calculus 113 | tee /usr/local/bin/binary-to-text >/dev/null <<"EOF" 114 | #!/usr/bin/env python3 115 | 116 | import re 117 | import sys 118 | 119 | text = re.sub(r"[^01]", "", sys.stdin.read()) 120 | out = [] 121 | 122 | for m in re.finditer(r"([01]{8})", text): 123 | out += chr(int(m.group(0), 2)) 124 | 125 | print("".join(out), end="") 126 | EOF 127 | chmod +x /usr/local/bin/binary-to-text 128 | 129 | # BrainF 130 | tee /usr/local/bin/brainf-repl >/dev/null <<"EOF" 131 | #!/usr/bin/env python3 132 | import argparse 133 | import readline 134 | import subprocess 135 | import tempfile 136 | 137 | parser = argparse.ArgumentParser() 138 | parser.add_argument("file", nargs="?") 139 | args = parser.parse_args() 140 | 141 | if args.file: 142 | subprocess.run(["beef", args.file]) 143 | while True: 144 | try: 145 | code = input("bf> ") 146 | except KeyboardInterrupt: 147 | print("^C") 148 | continue 149 | except EOFError: 150 | print("^D") 151 | break 152 | if not code: 153 | continue 154 | with tempfile.NamedTemporaryFile(mode="w") as f: 155 | f.write(code) 156 | f.flush() 157 | subprocess.run(["beef", f.name]) 158 | EOF 159 | chmod +x /usr/local/bin/brainf-repl 160 | 161 | # Cat 162 | tee /opt/cat/repl.js >/dev/null <<"EOF" 163 | const fs = require("fs"); 164 | const repl = require("repl"); 165 | 166 | const args = process.argv.slice(2); 167 | if (args.length > 1) { 168 | console.error("usage: repl.js [FILE]"); 169 | process.exit(1); 170 | } 171 | 172 | const program = args.length === 1 ? fs.readFileSync(args[0], "utf-8") : null; 173 | 174 | const cat = require("cat"); 175 | const ce = new cat.CatLanguage.CatEvaluator(); 176 | 177 | if (program !== null) { 178 | ce.eval(program); 179 | } 180 | 181 | repl.start({prompt: "cat> ", eval: (cmd, context, filename, callback) => callback(null, ce.eval(cmd))}); 182 | EOF 183 | 184 | # Haskell 185 | mkdir -p /opt/haskell 186 | tee /opt/haskell/hie.yaml >/dev/null <<"EOF" 187 | cradle: 188 | direct: 189 | arguments: [] 190 | EOF 191 | 192 | # Qalb 193 | mkdir -p /opt/qalb 194 | tee /opt/qalb/repl.js >/dev/null <<"EOF" 195 | const fs = require("fs"); 196 | const repl = require("repl"); 197 | 198 | const args = process.argv.slice(2); 199 | if (args.length > 1) { 200 | console.error("usage: repl.js [FILE]"); 201 | process.exit(1); 202 | } 203 | 204 | const program = args.length === 1 ? fs.readFileSync(args[0], "utf-8") : null; 205 | 206 | eval(fs.readFileSync("/opt/qalb/qlb.js", "utf-8")); 207 | eval(fs.readFileSync("/opt/qalb/parser.js", "utf-8")); 208 | eval(fs.readFileSync("/opt/qalb/primitives.js", "utf-8")); 209 | 210 | Qlb.init({console}); 211 | 212 | if (program !== null) { 213 | Qlb.execute(program); 214 | } 215 | 216 | repl.start({prompt: "قلب> ", eval: (cmd, context, filename, callback) => callback(null, Qlb.execute(cmd))}); 217 | EOF 218 | 219 | # Unlambda 220 | tee /usr/local/bin/unlambda-repl >/dev/null <<"EOF" 221 | #!/usr/bin/env python3 222 | import argparse 223 | import readline 224 | import subprocess 225 | 226 | parser = argparse.ArgumentParser() 227 | parser.add_argument("file", nargs="?") 228 | args = parser.parse_args() 229 | 230 | if args.file: 231 | with open(args.file) as f: 232 | subprocess.run(["unlambda"], input=f.read(), encoding="utf-8") 233 | while True: 234 | try: 235 | code = input("λ> ") 236 | except KeyboardInterrupt: 237 | print("^C") 238 | continue 239 | except EOFError: 240 | print("^D") 241 | break 242 | if not code: 243 | continue 244 | subprocess.run(["unlambda"], input=code, encoding="utf-8") 245 | EOF 246 | chmod +x /usr/local/bin/unlambda-repl 247 | 248 | userdel -r build 249 | popd >/dev/null 250 | rm "$0" 251 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Riju 2 | 3 | Riju is a very fast online playground for every programming language. 4 | In less than a second, you can start playing with a Python interpreter 5 | or compiling INTERCAL code. 6 | 7 | Check out the [live application](https://riju.codes/)! 8 | 9 | **You should not write any sensitive code on Riju, as NO GUARANTEES 10 | are made about the security or privacy of your data. (No warranty etc 11 | etc.)** 12 | 13 | This project is a work in progress, and I don't intend on thoroughly 14 | documenting it until it has reached feature-completeness. 15 | 16 | ## Criteria for language inclusion 17 | 18 | I aspire for Riju to support more languages than any reasonable person 19 | could conceivably think is reasonable. That said, there are some 20 | requirements: 21 | 22 | * **Language must have a clear notion of execution.** This is because 23 | a core part of Riju is the ability to execute code. Languages like 24 | [YAML](https://yaml.org/), [SCSS](https://sass-lang.com/), and 25 | Markdown are fine because they have a canonical transformation (into 26 | [JSON](https://www.json.org/json-en.html), 27 | [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS), and 28 | [HTML](https://developer.mozilla.org/en-US/docs/Web/HTML) 29 | respectively) that can be performed on execution. However, languages 30 | like JSON, CSS, and HTML are not acceptable, because there's nothing 31 | reasonable to do when they are run. 32 | * **Language must not require input or configuration.** This is 33 | because, in order to avoid bloating the interface, Riju provides a 34 | way to supply code but not any other data. Of course, it's possible 35 | to supply input interactively, so reading stdin is allowed, but if a 36 | language can only reasonably be programmed with additional input, 37 | it's not a candidate for inclusion. Thus, many templating languages 38 | are excluded, since they don't do anything unless you are 39 | substituting a value. However, some languages such as 40 | [Pug](https://pugjs.org/) are allowed, because they implement a 41 | significant syntax transformation outside of template substitution. 42 | Also, languages like [Sed](https://www.gnu.org/software/sed/) and 43 | [Awk](https://www.gnu.org/software/gawk/) are allowed, because it's 44 | straightforward to test code written in them even without a 45 | pre-prepared input file. 46 | * **Language must not require a graphical environment.** This is 47 | because we use a pty to run code, and there is no X forwarding. As 48 | such, we can't use languages like 49 | [Scratch](https://scratch.mit.edu/), 50 | [Alice](https://www.alice.org/), and 51 | [Linotte](http://langagelinotte.free.fr/wordpress/). 52 | * **Language must be available for free under a permissive license.** 53 | This is because we must download and install all languages 54 | noninteractively in the Docker image build, so anything that 55 | requires license registration is unlikely to work (or be legal). We 56 | can't use [Mathematica](https://www.wolfram.com/mathematica/) or 57 | [MATLAB](https://www.mathworks.com/products/matlab.html), for 58 | example, but we can use [Mathics](https://mathics.github.io/) and 59 | [Octave](https://www.gnu.org/software/octave/), which provide 60 | compatible open-source implementations of the underlying languages. 61 | * **Language must be runnable under Docker on Linux.** This is because 62 | that's the execution environment we have access to. 63 | [AppleScript](https://en.wikipedia.org/wiki/AppleScript) is out 64 | because it only runs on macOS, and [Docker](https://www.docker.com/) 65 | is out because it can't be run inside Docker (without the 66 | `--privileged` flag, which has unacceptable security drawbacks; see 67 | [#29](https://github.com/raxod502/riju/issues/29)). Note, however, 68 | that many Windows-based languages can be used successfully via 69 | [Mono](https://www.mono-project.com/) or 70 | [Wine](https://www.winehq.org/), such as 71 | [Cmd](https://en.wikipedia.org/wiki/Cmd.exe), 72 | [C#](https://en.wikipedia.org/wiki/C_Sharp_(programming_language)), 73 | and [Visual Basic](https://en.wikipedia.org/wiki/Visual_Basic). 74 | 75 | Here are some explicit *non-requirements*: 76 | 77 | * *Language must be well-known.* Nope, I'll be happy to add your pet 78 | project; after all, [Kalyn](https://github.com/raxod502/kalyn) and 79 | [Ink](https://github.com/thesephist/ink) are already supported. 80 | * *Language must be useful.* I would have no objection to adding 81 | everything on the esolangs wiki, if there are interpreters/compilers 82 | available. 83 | * *Language must be easy to install and run.* Well, it would be nice, 84 | but I've seen some s\*\*\* when adding languages to Riju so it will 85 | take a lot to surprise me at this point. 86 | 87 | If you'd like to request a new language, head to the [language support 88 | meta-issue](https://github.com/raxod502/riju/issues/24) and add a 89 | comment. Of course, if you actually want it to be added anytime soon, 90 | you should submit a pull request :) 91 | 92 | ## Project setup 93 | 94 | To run the webserver, all you need is Yarn. Just run `yarn install` as 95 | usual to install dependencies. For production, it's: 96 | 97 | $ yarn backend |- or run all three with 'yarn build' 98 | $ yarn frontend | 99 | $ yarn system | 100 | $ yarn server 101 | 102 | For development with file watching and automatic server rebooting and 103 | all that, it's: 104 | 105 | $ yarn backend-dev |- or run all four with 'yarn dev' 106 | $ yarn frontend-dev | 107 | $ yarn system-dev | 108 | $ yarn server-dev | 109 | 110 | The webserver listens on `localhost:6119`. Now, although the server 111 | itself will work, the only languages that will work are the ones that 112 | happen to be installed on your machine. (I'm sure you can find a few 113 | that are already.) Also, sandboxing using UNIX filesystem permissions 114 | will be disabled, because that requires root privileges. If you want 115 | to test with *all* the languages plus sandboxing (or you're working on 116 | adding a new language), then you need to use Docker. Running the app 117 | is exactly the same as before, you just have to jump into the 118 | container first: 119 | 120 | $ make docker 121 | 122 | Note that building the image typically requires over an hour and 20 GB 123 | of disk space, and it is only growing. 124 | 125 | The above command generates the development image as a subroutine. You 126 | can skip this and use the last tagged development image: 127 | 128 | $ make docker-nobuild 129 | 130 | Or you can explicitly build the image without running it: 131 | 132 | $ make image-dev 133 | 134 | The production image is based on the development one, with some 135 | additional layers. You can build it as follows: 136 | 137 | $ make image-prod 138 | 139 | Lastly I should mention the tests. There are integration tests for 140 | every language, and they can be run as follows: 141 | 142 | $ [CONCURRENCY=2] [TIMEOUT_FACTOR=1] yarn test [...] 143 | 144 | Filters can be for language (`python`, `java`) or test type (`hello`, 145 | `lsp`). You can comma-delimit multiple filters to do a disjunction, 146 | and space-delimit them to do a conjunction (`yarn test hello 147 | python,java` for the `hello` tests for `python` and `java`). 148 | 149 | The tests are run automatically when building the production image, 150 | and fail the build if they fail. 151 | 152 | See also [riju-cdn](https://github.com/raxod502/riju-cdn). 153 | 154 | ## Flag 155 | 156 | [![Flag](flag.png)](https://www.reddit.com/r/Breath_of_the_Wild/comments/947ewf/flag_of_the_gerudo_based_on_the_flag_of_kazakhstan/) 157 | -------------------------------------------------------------------------------- /frontend/src/app.ts: -------------------------------------------------------------------------------- 1 | import * as monaco from "monaco-editor"; 2 | import { 3 | createConnection, 4 | MonacoLanguageClient, 5 | MonacoServices, 6 | Services, 7 | } from "monaco-languageclient"; 8 | import { Disposable } from "vscode"; 9 | import { createMessageConnection } from "vscode-jsonrpc"; 10 | import { 11 | AbstractMessageReader, 12 | DataCallback, 13 | } from "vscode-jsonrpc/lib/messageReader"; 14 | import { AbstractMessageWriter } from "vscode-jsonrpc/lib/messageWriter"; 15 | import { Message } from "vscode-jsonrpc/lib/messages"; 16 | import { Terminal } from "xterm"; 17 | import { FitAddon } from "xterm-addon-fit"; 18 | 19 | import "xterm/css/xterm.css"; 20 | 21 | const DEBUG = window.location.hash === "#debug"; 22 | const config: RijuConfig = (window as any).rijuConfig; 23 | 24 | interface RijuConfig { 25 | id: string; 26 | monacoLang?: string; 27 | main: string; 28 | format?: any; 29 | lsp?: { 30 | disableDynamicRegistration?: boolean; 31 | init?: any; 32 | config?: any; 33 | lang?: string; 34 | }; 35 | template: string; 36 | } 37 | 38 | class RijuMessageReader extends AbstractMessageReader { 39 | state: "initial" | "listening" | "closed" = "initial"; 40 | callback: DataCallback | null = null; 41 | messageQueue: any[] = []; 42 | socket: WebSocket; 43 | 44 | constructor(socket: WebSocket) { 45 | super(); 46 | this.socket = socket; 47 | this.socket.addEventListener("message", (event: MessageEvent) => { 48 | this.readMessage(event.data); 49 | }); 50 | } 51 | 52 | listen(callback: DataCallback): void { 53 | if (this.state === "initial") { 54 | this.state = "listening"; 55 | this.callback = callback; 56 | while (this.messageQueue.length > 0) { 57 | this.readMessage(this.messageQueue.pop()!); 58 | } 59 | } 60 | } 61 | 62 | readMessage(rawMessage: string): void { 63 | if (this.state === "initial") { 64 | this.messageQueue.splice(0, 0, rawMessage); 65 | } else if (this.state === "listening") { 66 | let message: any; 67 | try { 68 | message = JSON.parse(rawMessage); 69 | } catch (err) { 70 | return; 71 | } 72 | switch (message && message.event) { 73 | case "lspOutput": 74 | if (DEBUG) { 75 | console.log("RECEIVE LSP:", message.output); 76 | } 77 | this.callback!(message.output); 78 | break; 79 | } 80 | } 81 | } 82 | } 83 | 84 | class RijuMessageWriter extends AbstractMessageWriter { 85 | socket: WebSocket; 86 | 87 | constructor(socket: WebSocket) { 88 | super(); 89 | this.socket = socket; 90 | } 91 | 92 | write(msg: Message): void { 93 | switch ((msg as any).method) { 94 | case "initialize": 95 | (msg as any).params.processId = null; 96 | if (config.lsp!.disableDynamicRegistration) { 97 | this.disableDynamicRegistration(msg); 98 | } 99 | break; 100 | case "textDocument/didOpen": 101 | if (config.lsp!.lang) { 102 | (msg as any).params.textDocument.languageId = config.lsp!.lang; 103 | } 104 | } 105 | if (DEBUG) { 106 | console.log("SEND LSP:", msg); 107 | } 108 | this.socket.send(JSON.stringify({ event: "lspInput", input: msg })); 109 | } 110 | 111 | disableDynamicRegistration(msg: any) { 112 | if (!msg || typeof msg !== "object") return; 113 | for (const [key, val] of Object.entries(msg)) { 114 | if (key === "dynamicRegistration" && val === true) 115 | msg.dynamicRegistration = false; 116 | this.disableDynamicRegistration(val); 117 | } 118 | } 119 | } 120 | 121 | async function main() { 122 | const term = new Terminal(); 123 | const fitAddon = new FitAddon(); 124 | term.loadAddon(fitAddon); 125 | term.open(document.getElementById("terminal")!); 126 | 127 | fitAddon.fit(); 128 | window.addEventListener("resize", () => fitAddon.fit()); 129 | 130 | await new Promise((resolve) => 131 | term.write("Connecting to server...", resolve) 132 | ); 133 | 134 | const initialRetryDelayMs = 200; 135 | let retryDelayMs = initialRetryDelayMs; 136 | 137 | function sendMessage(message: any) { 138 | if (DEBUG) { 139 | console.log("SEND:", message); 140 | } 141 | if (socket) { 142 | socket.send(JSON.stringify(message)); 143 | } 144 | } 145 | 146 | function tryConnect() { 147 | let clientDisposable: Disposable | null = null; 148 | let servicesDisposable: Disposable | null = null; 149 | const serviceLogBuffers: { [index: string]: string } = {}; 150 | console.log("Connecting to server..."); 151 | socket = new WebSocket( 152 | (document.location.protocol === "http:" ? "ws://" : "wss://") + 153 | document.location.host + 154 | `/api/v1/ws?lang=${encodeURIComponent(config.id)}` 155 | ); 156 | socket.addEventListener("open", () => { 157 | console.log("Successfully connected to server"); 158 | }); 159 | socket.addEventListener("message", (event: MessageEvent) => { 160 | let message: any; 161 | try { 162 | message = JSON.parse(event.data); 163 | } catch (err) { 164 | console.error("Malformed message from server:", event.data); 165 | return; 166 | } 167 | if ( 168 | DEBUG && 169 | message && 170 | message.event !== "lspOutput" && 171 | message.event !== "serviceLog" 172 | ) { 173 | console.log("RECEIVE:", message); 174 | } 175 | if (message && message.event && message.event !== "error") { 176 | retryDelayMs = initialRetryDelayMs; 177 | } 178 | switch (message && message.event) { 179 | case "terminalClear": 180 | term.reset(); 181 | return; 182 | case "terminalOutput": 183 | if (typeof message.output !== "string") { 184 | console.error("Unexpected message from server:", message); 185 | return; 186 | } 187 | term.write(message.output); 188 | return; 189 | case "formattedCode": 190 | if ( 191 | typeof message.code !== "string" || 192 | typeof message.originalCode !== "string" 193 | ) { 194 | console.error("Unexpected message from server:", message); 195 | return; 196 | } 197 | if (editor.getValue() === message.originalCode) { 198 | editor.setValue(message.code); 199 | } 200 | return; 201 | case "lspStarted": 202 | if (typeof message.root !== "string") { 203 | console.error("Unexpected message from server:", message); 204 | return; 205 | } 206 | const services = MonacoServices.create(editor as any, { 207 | rootUri: `file://${message.root}`, 208 | }); 209 | servicesDisposable = Services.install(services); 210 | editor.setModel( 211 | monaco.editor.createModel( 212 | editor.getModel()!.getValue(), 213 | undefined, 214 | monaco.Uri.parse(`file://${message.root}/${config.main}`) 215 | ) 216 | ); 217 | const connection = createMessageConnection( 218 | new RijuMessageReader(socket!), 219 | new RijuMessageWriter(socket!) 220 | ); 221 | const client = new MonacoLanguageClient({ 222 | name: "Riju", 223 | clientOptions: { 224 | documentSelector: [{ pattern: "**" }], 225 | middleware: { 226 | workspace: { 227 | configuration: ( 228 | params: any, 229 | token: any, 230 | configuration: any 231 | ) => { 232 | return Array( 233 | (configuration(params, token) as {}[]).length 234 | ).fill( 235 | config.lsp!.config !== undefined ? config.lsp!.config : {} 236 | ); 237 | }, 238 | }, 239 | }, 240 | initializationOptions: config.lsp!.init || {}, 241 | }, 242 | connectionProvider: { 243 | get: (errorHandler: any, closeHandler: any) => 244 | Promise.resolve( 245 | createConnection(connection, errorHandler, closeHandler) 246 | ), 247 | }, 248 | }); 249 | clientDisposable = client.start(); 250 | return; 251 | case "lspOutput": 252 | // Should be handled by RijuMessageReader 253 | return; 254 | case "serviceLog": 255 | if ( 256 | typeof message.service !== "string" || 257 | typeof message.output !== "string" 258 | ) { 259 | console.error("Unexpected message from server:", message); 260 | return; 261 | } 262 | if (DEBUG) { 263 | let buffer = serviceLogBuffers[message.service] || ""; 264 | buffer += message.output; 265 | while (buffer.includes("\n")) { 266 | const idx = buffer.indexOf("\n"); 267 | const line = buffer.slice(0, idx); 268 | buffer = buffer.slice(idx + 1); 269 | console.log(`${message.service.toUpperCase()} || ${line}`); 270 | } 271 | serviceLogBuffers[message.service] = buffer; 272 | } 273 | return; 274 | case "serviceCrashed": 275 | return; 276 | default: 277 | console.error("Unexpected message from server:", message); 278 | return; 279 | } 280 | }); 281 | socket.addEventListener("close", (event: CloseEvent) => { 282 | if (event.wasClean) { 283 | console.log("Connection closed cleanly"); 284 | } else { 285 | console.error("Connection died"); 286 | } 287 | if (clientDisposable) { 288 | clientDisposable.dispose(); 289 | clientDisposable = null; 290 | } 291 | if (servicesDisposable) { 292 | servicesDisposable.dispose(); 293 | servicesDisposable = null; 294 | } 295 | scheduleConnect(); 296 | }); 297 | } 298 | 299 | function scheduleConnect() { 300 | const delay = retryDelayMs * Math.random(); 301 | console.log(`Trying to reconnect in ${Math.floor(delay)}ms`); 302 | setTimeout(tryConnect, delay); 303 | retryDelayMs *= 2; 304 | } 305 | 306 | let socket: WebSocket | null = null; 307 | tryConnect(); 308 | 309 | term.onData((data) => sendMessage({ event: "terminalInput", input: data })); 310 | 311 | const editor = monaco.editor.create(document.getElementById("editor")!, { 312 | minimap: { enabled: false }, 313 | scrollbar: { verticalScrollbarSize: 0 }, 314 | }); 315 | window.addEventListener("resize", () => editor.layout()); 316 | editor.getModel()!.setValue(config.template); 317 | monaco.editor.setModelLanguage( 318 | editor.getModel()!, 319 | config.monacoLang || "plaintext" 320 | ); 321 | 322 | document.getElementById("runButton")!.addEventListener("click", () => { 323 | sendMessage({ event: "runCode", code: editor.getValue() }); 324 | }); 325 | if (config.format) { 326 | document.getElementById("formatButton")!.classList.add("visible"); 327 | document.getElementById("formatButton")!.addEventListener("click", () => { 328 | sendMessage({ event: "formatCode", code: editor.getValue() }); 329 | }); 330 | } 331 | } 332 | 333 | main().catch(console.error); 334 | -------------------------------------------------------------------------------- /scripts/docker-install-phase4.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -o pipefail 5 | set -x 6 | pushd /tmp >/dev/null 7 | 8 | latest_release() { 9 | curl -sSL "https://api.github.com/repos/$1/releases/latest" | jq -r .tag_name 10 | } 11 | 12 | # Needed for project infrastructure 13 | ver="$(latest_release watchexec/watchexec)" 14 | wget -nv "https://github.com/watchexec/watchexec/releases/download/${ver}/watchexec-${ver}-x86_64-unknown-linux-gnu.deb" 15 | dpkg -i watchexec-*.deb 16 | rm watchexec-*.deb 17 | 18 | # Shared 19 | ver="$(latest_release jgm/pandoc)" 20 | wget -nv "https://github.com/jgm/pandoc/releases/download/${ver}/pandoc-${ver}-linux-amd64.tar.gz" 21 | tar -xf pandoc-*-linux-amd64.tar.gz -C /usr --strip-components=1 22 | rm pandoc-*-linux-amd64.tar.gz 23 | 24 | # ><> 25 | wget -nv https://gist.githubusercontent.com/anonymous/6392418/raw/fish.py -O /usr/local/bin/esofish 26 | sed -i 's:^#!.*:#!/usr/bin/env python3:' /usr/local/bin/esofish 27 | chmod +x /usr/local/bin/esofish 28 | 29 | # Ada 30 | wget -nv https://dl.bintray.com/reznikmm/ada-language-server/linux-latest.tar.gz 31 | tar -xf linux-latest.tar.gz 32 | mv linux/ada_language_server /usr/local/bin/ada_language_server 33 | mv linux/*.so* /usr/lib/x86_64-linux-gnu/ 34 | rm -rf linux linux-latest.tar.gz 35 | 36 | # APL 37 | file="$(curl -sS ftp://ftp.gnu.org/gnu/apl/ | grep -Eo 'apl_[-0-9.]+_amd64.deb$' | sort -rV | head -n1)" 38 | wget -nv "ftp://ftp.gnu.org/gnu/apl/${file}" 39 | dpkg -i apl_*_amd64.deb 40 | rm apl_*_amd64.deb 41 | 42 | # Boo 43 | wget -nv https://github.com/boo-lang/boo/releases/download/unstable/boo-latest.zip 44 | unzip boo-latest.zip 45 | mv boo-latest /usr/local/lib/boo 46 | chmod +x /usr/local/lib/boo/booc /usr/local/lib/boo/booish 47 | ln -s /usr/local/lib/boo/booc /usr/local/lib/boo/booish /usr/local/bin/ 48 | 49 | # Clojure 50 | ver="$(latest_release snoe/clojure-lsp)" 51 | wget -nv "https://github.com/snoe/clojure-lsp/releases/download/${ver}/clojure-lsp" 52 | chmod +x clojure-lsp 53 | mv clojure-lsp /usr/local/bin/clojure-lsp 54 | 55 | # D 56 | wget -nv "$(curl -sSL https://dlang.org/download.html | grep -Eo '"http://[^"]+amd64.deb"' | tr -d '"')" 57 | dpkg -i dmd_*.deb 58 | rm dmd_*.deb 59 | 60 | # Dhall 61 | ver="$(latest_release dhall-lang/dhall-haskell)" 62 | file="$(curl -sSL "https://api.github.com/repos/dhall-lang/dhall-haskell/releases/tags/${ver}" | jq -r '.assets | map(select(.name | (contains("dhall-json") and contains("x86_64-linux.tar.bz2")))) | .[0].name')" 63 | wget -nv "https://github.com/dhall-lang/dhall-haskell/releases/download/${ver}/${file}" 64 | mkdir dhall-json 65 | tar -xf dhall-json-*-x86_64-linux.tar.bz2 -C dhall-json 66 | mv dhall-json/bin/dhall-to-json dhall-json/bin/json-to-dhall /usr/local/bin/ 67 | rm -rf dhall-json dhall-json-*-x86_64-linux.tar.bz2 68 | 69 | # Dylan 70 | ver="$(latest_release dylan-lang/opendylan)" 71 | wget -nv "https://github.com/dylan-lang/opendylan/releases/download/${ver}/opendylan-$(grep -Eo '[0-9]+\.[0-9]+' <<< "$ver")-x86_64-linux.tar.bz2" 72 | tar -xf opendylan-*-x86_64-linux.tar.bz2 73 | rm opendylan-*-x86_64-linux.tar.bz2 74 | mv opendylan-* /opt/dylan 75 | ln -s /opt/dylan/bin/dylan-compiler /opt/dylan/bin/make-dylan-app /usr/local/bin/ 76 | 77 | # Elixir 78 | ver="$(latest_release elixir-lsp/elixir-ls)" 79 | wget -nv "https://github.com/elixir-lsp/elixir-ls/releases/download/${ver}/elixir-ls.zip" 80 | unzip -d /opt/elixir-ls elixir-ls.zip 81 | ln -s /opt/elixir-ls/language_server.sh /usr/local/bin/elixir-ls 82 | rm elixir-ls.zip 83 | 84 | # Elm 85 | ver="$(latest_release elm/compiler)" 86 | wget -nv "https://github.com/elm/compiler/releases/download/${ver}/binary-for-linux-64-bit.gz" 87 | gunzip binary-for-linux-64-bit.gz 88 | chmod +x binary-for-linux-64-bit 89 | mv binary-for-linux-64-bit /usr/local/bin/elm 90 | 91 | # Emojicode 92 | ver="$(latest_release emojicode/emojicode)" 93 | wget -nv "https://github.com/emojicode/emojicode/releases/download/${ver}/Emojicode-$(sed 's/^v//' <<< "$ver")-Linux-x86_64.tar.gz" 94 | tar -xf Emojicode-*-Linux-x86_64.tar.gz 95 | pushd Emojicode-*-Linux-x86_64 >/dev/null 96 | mv emojicodec /usr/local/bin/ 97 | mkdir -p /usr/local/include/emojicode 98 | mv include/* /usr/local/include/emojicode/ 99 | mkdir -p /usr/local/EmojicodePackages 100 | mv packages/* /usr/local/EmojicodePackages/ 101 | popd >/dev/null 102 | rm -rf Emojicode-*-Linux-x86_64 Emojicode-*-Linux-x86_64.tar.gz 103 | 104 | # Entropy 105 | wget -nv http://danieltemkin.com/Content/Entropy/Entropy.zip 106 | unzip -d /opt/entropy Entropy.zip 107 | rm Entropy.zip 108 | 109 | # Erlang 110 | wget -nv https://s3.amazonaws.com/rebar3/rebar3 111 | chmod +x rebar3 112 | mv rebar3 /usr/local/bin/rebar3 113 | 114 | # Euphoria 115 | wget -nv http://www.rapideuphoria.com/31/euphor31.tar 116 | mkdir /opt/euphoria 117 | tar -xf euphor*.tar -C /opt/euphoria --strip-components=1 118 | ln -s /opt/euphoria/bin/exu /usr/bin/ 119 | rm euphor*.tar 120 | 121 | # Ezhil 122 | wget -nv https://github.com/raxod502/riju-cdn/releases/download/ezhil-2017.08.19/ezhil.tar.gz 123 | tar -xf ezhil.tar.gz 124 | mv ezhil-* /opt/ezhil 125 | cp /opt/ezhil/ezhili /opt/ezhil/ezhuthi/ 126 | ln -s /opt/ezhil/ezhuthi/ezhili /usr/local/bin/ 127 | rm ezhil.tar.gz 128 | 129 | # Factor 130 | ver="$(curl -sSL https://factorcode.org/ | grep -Eo 'release\?os=linux[^>]+>[^<]+' | sed -E 's/[^>]+>//' | head -n1)" 131 | wget -nv "https://downloads.factorcode.org/releases/${ver}/factor-linux-x86-64-${ver}.tar.gz" 132 | tar -xf factor-linux-x86-64-*.tar.gz 133 | mv -T factor /opt/factor 134 | ln -s /opt/factor/factor /usr/local/bin/factor-lang 135 | rm factor-linux-x86-64-*.tar.gz 136 | 137 | # Go 138 | export GO111MODULE=on 139 | export GOPATH="$PWD/go" 140 | go get golang.org/x/tools/gopls@latest 141 | mv go/bin/gopls /usr/local/bin/gopls 142 | rm -rf go 143 | 144 | # GolfScript 145 | wget -nv http://www.golfscript.com/golfscript/golfscript.rb -O /usr/local/bin/golfscript 146 | chmod +x /usr/local/bin/golfscript 147 | 148 | # Haskell 149 | curl -sSL https://get.haskellstack.org/ | sh 150 | wget -nv https://github.com/raxod502/riju-cdn/releases/download/brittany-0.12.1.1/brittany -O /usr/local/bin/brittany 151 | chmod +x /usr/local/bin/brittany 152 | 153 | mkdir -p /opt/haskell 154 | wget -nv https://github.com/raxod502/riju-cdn/releases/download/hie-1.4-a9005b2ba2050bdfdd4438f1d471a3f7985492cd-ghc8.6.5/hie -O /usr/local/bin/hie 155 | wget -nv https://github.com/raxod502/riju-cdn/releases/download/hie-1.4-a9005b2ba2050bdfdd4438f1d471a3f7985492cd-ghc8.6.5/hoogle.hoo -O /opt/haskell/hoogle.hoo 156 | chmod +x /usr/local/bin/hie 157 | 158 | # HCL/TOML/YAML 159 | ver="$(latest_release sclevine/yj)" 160 | wget -nv "https://github.com/sclevine/yj/releases/download/${ver}/yj-linux" 161 | chmod +x yj-linux 162 | mv yj-linux /usr/local/bin/yj 163 | 164 | # Ink 165 | ver="$(latest_release thesephist/ink)" 166 | wget -nv "https://github.com/thesephist/ink/releases/download/${ver}/ink-linux" 167 | wget -nv "https://github.com/thesephist/ink/releases/download/${ver}/std.ink" 168 | wget -nv "https://github.com/thesephist/ink/releases/download/${ver}/str.ink" 169 | chmod +x ink-linux 170 | mv ink-linux /usr/local/bin/ink 171 | mkdir /opt/ink 172 | mv std.ink str.ink /opt/ink/ 173 | 174 | # Ioke 175 | wget -nv https://ioke.org/dist/ioke-ikj-latest.tar.gz 176 | tar -xf ioke-ikj-*.tar.gz -C /opt 177 | rm ioke-ikj-*.tar.gz 178 | ln -s /opt/ioke/bin/ioke /usr/local/bin/ioke 179 | 180 | # J 181 | wget -nv "$(curl -sSL https://code.jsoftware.com/wiki/System/Installation/J901/Debian | grep -F '/dev/null <<"EOF" 260 | #!/usr/bin/env bash 261 | RUSTUP_HOME=/opt/rust exec /opt/rust/bin/${0##*/} "$@" 262 | EOF 263 | chmod +x /opt/rust/wrapper 264 | for file in /opt/rust/bin/*; do 265 | ln -s /opt/rust/wrapper "/usr/local/bin/${file##*/}" 266 | done 267 | 268 | # SETL 269 | wget -nv https://setl.org/setl/bin/Linux-x86-64bit/setlbin.tgz 270 | tar -xf setlbin.tgz -C /usr/local/bin 271 | 272 | # SQL 273 | ver="$(latest_release lighttiger2505/sqls)" 274 | wget -nv "https://github.com/lighttiger2505/sqls/releases/download/${ver}/sqls-${ver}-linux-amd64.tar.gz" 275 | tar -xf sqls-*-linux-amd64.tar.gz 276 | mv linux-amd64/sqls /usr/local/bin/ 277 | rm -rf linux-amd64 sqls-*-linux-amd64.tar.gz 278 | 279 | # Swift 280 | wget -nv https://github.com/raxod502/riju-cdn/releases/download/swift-5.2.4-20.04/swift.tar.gz -O swift.tar.gz 281 | mkdir /opt/swift 282 | tar -xf swift.tar.gz -C /opt/swift --strip-components=2 283 | ln -s /opt/swift/bin/swiftc /usr/local/bin/swiftc 284 | ln -s /opt/swift/bin/sourcekit-lsp /usr/local/bin/sourcekit-lsp 285 | rm swift.tar.gz 286 | 287 | # Unison 288 | wget -nv https://github.com/raxod502/riju-cdn/releases/download/unison-M1l-232-519cbeb58704c1b9410c9386e492be59fd5a5334/unison -O /usr/local/bin/unison 289 | chmod +x /usr/local/bin/unison 290 | 291 | popd >/dev/null 292 | rm "$0" 293 | -------------------------------------------------------------------------------- /backend/src/api.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess, spawn } from "child_process"; 2 | import * as path from "path"; 3 | import * as WebSocket from "ws"; 4 | 5 | import * as pty from "node-pty"; 6 | import { IPty } from "node-pty"; 7 | import PQueue from "p-queue"; 8 | import * as rpc from "vscode-jsonrpc"; 9 | import { v4 as getUUID } from "uuid"; 10 | 11 | import { LangConfig, langs } from "./langs"; 12 | import { borrowUser } from "./users"; 13 | import * as util from "./util"; 14 | import { Context, Options, bash } from "./util"; 15 | 16 | const allSessions: Set = new Set(); 17 | 18 | export class Session { 19 | ws: WebSocket; 20 | uuid: string; 21 | lang: string; 22 | 23 | tearingDown: boolean = false; 24 | 25 | // Initialized by setup() 26 | uidInfo: { 27 | uid: number; 28 | returnUID: () => Promise; 29 | } | null = null; 30 | 31 | // Initialized later or never 32 | term: { pty: IPty; live: boolean } | null = null; 33 | lsp: { 34 | proc: ChildProcess; 35 | reader: rpc.StreamMessageReader; 36 | writer: rpc.StreamMessageWriter; 37 | } | null = null; 38 | daemon: { proc: ChildProcess } | null = null; 39 | formatter: { 40 | proc: ChildProcess; 41 | live: boolean; 42 | input: string; 43 | output: string; 44 | } | null = null; 45 | 46 | logPrimitive: (msg: string) => void; 47 | 48 | msgQueue: PQueue = new PQueue({ concurrency: 1 }); 49 | 50 | get homedir() { 51 | return `/tmp/riju/${this.uuid}`; 52 | } 53 | 54 | get config() { 55 | return langs[this.lang]; 56 | } 57 | 58 | get uid() { 59 | return this.uidInfo!.uid; 60 | } 61 | 62 | returnUID = async () => { 63 | this.uidInfo && (await this.uidInfo.returnUID()); 64 | }; 65 | 66 | get context() { 67 | return { uid: this.uid, uuid: this.uuid }; 68 | } 69 | 70 | log = (msg: string) => this.logPrimitive(`[${this.uuid}] ${msg}`); 71 | 72 | constructor(ws: WebSocket, lang: string, log: (msg: string) => void) { 73 | this.ws = ws; 74 | this.uuid = getUUID(); 75 | this.lang = lang; 76 | this.logPrimitive = log; 77 | this.log(`Creating session, language ${this.lang}`); 78 | } 79 | 80 | run = async (args: string[], options?: Options) => { 81 | return await util.run(args, this.log, options); 82 | }; 83 | 84 | privilegedSetup = () => util.privilegedSetup(this.context); 85 | privilegedSpawn = (args: string[]) => 86 | util.privilegedSpawn(this.context, args); 87 | privilegedUseradd = () => util.privilegedUseradd(this.uid); 88 | privilegedTeardown = () => util.privilegedTeardown(this.context); 89 | 90 | setup = async () => { 91 | try { 92 | allSessions.add(this); 93 | const { uid, returnUID } = await borrowUser(this.log); 94 | this.uidInfo = { uid, returnUID }; 95 | this.log(`Borrowed uid ${this.uid}`); 96 | await this.run(this.privilegedSetup()); 97 | if (this.config.setup) { 98 | await this.run(this.privilegedSpawn(bash(this.config.setup))); 99 | } 100 | await this.runCode(); 101 | if (this.config.daemon) { 102 | const daemonArgs = this.privilegedSpawn(bash(this.config.daemon)); 103 | const daemonProc = spawn(daemonArgs[0], daemonArgs.slice(1)); 104 | this.daemon = { 105 | proc: daemonProc, 106 | }; 107 | for (const stream of [daemonProc.stdout, daemonProc.stderr]) { 108 | stream.on("data", (data) => 109 | this.send({ 110 | event: "serviceLog", 111 | service: "daemon", 112 | output: data.toString("utf8"), 113 | }) 114 | ); 115 | daemonProc.on("close", (code, signal) => 116 | this.send({ 117 | event: "serviceFailed", 118 | service: "daemon", 119 | error: `Exited with status ${signal || code}`, 120 | }) 121 | ); 122 | daemonProc.on("error", (err) => 123 | this.send({ 124 | event: "serviceFailed", 125 | service: "daemon", 126 | error: `${err}`, 127 | }) 128 | ); 129 | } 130 | } 131 | if (this.config.lsp) { 132 | if (this.config.lsp.setup) { 133 | await this.run(this.privilegedSpawn(bash(this.config.lsp.setup))); 134 | } 135 | const lspArgs = this.privilegedSpawn(bash(this.config.lsp.start)); 136 | const lspProc = spawn(lspArgs[0], lspArgs.slice(1)); 137 | this.lsp = { 138 | proc: lspProc, 139 | reader: new rpc.StreamMessageReader(lspProc.stdout), 140 | writer: new rpc.StreamMessageWriter(lspProc.stdin), 141 | }; 142 | this.lsp.reader.listen((data: any) => { 143 | this.send({ event: "lspOutput", output: data }); 144 | }); 145 | lspProc.stderr.on("data", (data) => 146 | this.send({ 147 | event: "serviceLog", 148 | service: "lsp", 149 | output: data.toString("utf8"), 150 | }) 151 | ); 152 | lspProc.on("close", (code, signal) => 153 | this.send({ 154 | event: "serviceFailed", 155 | service: "lsp", 156 | error: `Exited with status ${signal || code}`, 157 | }) 158 | ); 159 | lspProc.on("error", (err) => 160 | this.send({ event: "serviceFailed", service: "lsp", error: `${err}` }) 161 | ); 162 | this.send({ event: "lspStarted", root: this.homedir }); 163 | } 164 | this.ws.on("message", (msg: string) => 165 | this.msgQueue.add(() => this.receive(msg)) 166 | ); 167 | this.ws.on("close", async () => { 168 | await this.teardown(); 169 | }); 170 | this.ws.on("error", async (err) => { 171 | this.log(`Websocket error: ${err}`); 172 | await this.teardown(); 173 | }); 174 | } catch (err) { 175 | this.log(`Error while setting up environment`); 176 | console.log(err); 177 | this.sendError(err); 178 | await this.teardown(); 179 | } 180 | }; 181 | 182 | send = async (msg: any) => { 183 | try { 184 | if (this.tearingDown) { 185 | return; 186 | } 187 | this.ws.send(JSON.stringify(msg)); 188 | } catch (err) { 189 | this.log(`Failed to send websocket message: ${err}`); 190 | console.log(err); 191 | await this.teardown(); 192 | } 193 | }; 194 | 195 | sendError = async (err: any) => { 196 | await this.send({ event: "terminalClear" }); 197 | await this.send({ 198 | event: "terminalOutput", 199 | output: `Riju encountered an unexpected error: ${err} 200 | \r 201 | \rYou may want to save your code and refresh the page. 202 | `, 203 | }); 204 | }; 205 | 206 | logBadMessage = (msg: any) => { 207 | this.log(`Got malformed message from client: ${JSON.stringify(msg)}`); 208 | }; 209 | 210 | receive = async (event: string) => { 211 | try { 212 | if (this.tearingDown) { 213 | return; 214 | } 215 | let msg: any; 216 | try { 217 | msg = JSON.parse(event); 218 | } catch (err) { 219 | this.log(`Failed to parse message from client: ${event}`); 220 | return; 221 | } 222 | switch (msg && msg.event) { 223 | case "terminalInput": 224 | if (typeof msg.input !== "string") { 225 | this.logBadMessage(msg); 226 | break; 227 | } 228 | if (!this.term) { 229 | this.log("terminalInput ignored because term is null"); 230 | break; 231 | } 232 | this.term!.pty.write(msg.input); 233 | break; 234 | case "runCode": 235 | if (typeof msg.code !== "string") { 236 | this.logBadMessage(msg); 237 | break; 238 | } 239 | await this.runCode(msg.code); 240 | break; 241 | case "formatCode": 242 | if (typeof msg.code !== "string") { 243 | this.logBadMessage(msg); 244 | break; 245 | } 246 | await this.formatCode(msg.code); 247 | break; 248 | case "lspInput": 249 | if (typeof msg.input !== "object" || !msg) { 250 | this.logBadMessage(msg); 251 | break; 252 | } 253 | if (!this.lsp) { 254 | this.log(`lspInput ignored because lsp is null`); 255 | break; 256 | } 257 | this.lsp.writer.write(msg.input); 258 | break; 259 | case "ensure": 260 | if (!this.config.ensure) { 261 | this.log(`ensure ignored because of missing configuration`); 262 | break; 263 | } 264 | await this.ensure(this.config.ensure); 265 | break; 266 | default: 267 | this.logBadMessage(msg); 268 | break; 269 | } 270 | } catch (err) { 271 | this.log(`Error while handling message from client`); 272 | console.log(err); 273 | this.sendError(err); 274 | } 275 | }; 276 | 277 | writeCode = async (code: string) => { 278 | if (this.config.main.includes("/")) { 279 | await this.run( 280 | this.privilegedSpawn([ 281 | "mkdir", 282 | "-p", 283 | path.dirname(`${this.homedir}/${this.config.main}`), 284 | ]) 285 | ); 286 | } 287 | await this.run( 288 | this.privilegedSpawn([ 289 | "sh", 290 | "-c", 291 | `cat > ${path.resolve(this.homedir, this.config.main)}`, 292 | ]), 293 | { input: code } 294 | ); 295 | }; 296 | 297 | runCode = async (code?: string) => { 298 | try { 299 | const { 300 | name, 301 | repl, 302 | main, 303 | suffix, 304 | createEmpty, 305 | compile, 306 | run, 307 | template, 308 | } = this.config; 309 | if (this.term) { 310 | const pid = this.term.pty.pid; 311 | const args = this.privilegedSpawn( 312 | bash(`kill -SIGTERM ${pid}; sleep 1; kill -SIGKILL ${pid}`) 313 | ); 314 | spawn(args[0], args.slice(1)); 315 | // Signal to terminalOutput message generator using closure. 316 | this.term.live = false; 317 | this.term = null; 318 | } 319 | this.send({ event: "terminalClear" }); 320 | let cmdline: string; 321 | if (code) { 322 | cmdline = run; 323 | if (compile) { 324 | cmdline = `( ${compile} ) && ( ${run} )`; 325 | } 326 | } else if (repl) { 327 | cmdline = repl; 328 | } else { 329 | cmdline = `echo '${name} has no REPL, press Run to see it in action'`; 330 | } 331 | if (code === undefined) { 332 | code = createEmpty !== undefined ? createEmpty : template; 333 | } 334 | if (code && suffix) { 335 | code += suffix; 336 | } 337 | await this.writeCode(code); 338 | const termArgs = this.privilegedSpawn(bash(cmdline)); 339 | const term = { 340 | pty: pty.spawn(termArgs[0], termArgs.slice(1), { 341 | name: "xterm-color", 342 | }), 343 | live: true, 344 | }; 345 | this.term = term; 346 | this.term.pty.on("data", (data) => { 347 | // Capture term in closure so that we don't keep sending output 348 | // from the old pty even after it's been killed (see ghci). 349 | if (term.live) { 350 | this.send({ event: "terminalOutput", output: data }); 351 | } 352 | }); 353 | this.term.pty.on("exit", (code, signal) => { 354 | if (term.live) { 355 | this.send({ 356 | event: "serviceFailed", 357 | service: "terminal", 358 | error: `Exited with status ${signal || code}`, 359 | }); 360 | } 361 | }); 362 | } catch (err) { 363 | this.log(`Error while running user code`); 364 | console.log(err); 365 | this.sendError(err); 366 | } 367 | }; 368 | 369 | formatCode = async (code: string) => { 370 | try { 371 | if (!this.config.format) { 372 | this.log("formatCode ignored because format is null"); 373 | return; 374 | } 375 | if (this.formatter) { 376 | const pid = this.formatter.proc.pid; 377 | const args = this.privilegedSpawn( 378 | bash(`kill -SIGTERM ${pid}; sleep 1; kill -SIGKILL ${pid}`) 379 | ); 380 | spawn(args[0], args.slice(1)); 381 | this.formatter.live = false; 382 | this.formatter = null; 383 | } 384 | const args = this.privilegedSpawn(bash(this.config.format.run)); 385 | const formatter = { 386 | proc: spawn(args[0], args.slice(1)), 387 | live: true, 388 | input: code, 389 | output: "", 390 | }; 391 | formatter.proc.stdin!.end(code); 392 | formatter.proc.stdout!.on("data", (data) => { 393 | if (!formatter.live) return; 394 | formatter.output += data.toString("utf8"); 395 | }); 396 | formatter.proc.stderr!.on("data", (data) => { 397 | if (!formatter.live) return; 398 | this.send({ 399 | event: "serviceLog", 400 | service: "formatter", 401 | output: data.toString("utf8"), 402 | }); 403 | }); 404 | formatter.proc.on("close", (code, signal) => { 405 | if (!formatter.live) return; 406 | if (code === 0) { 407 | this.send({ 408 | event: "formattedCode", 409 | code: formatter.output, 410 | originalCode: formatter.input, 411 | }); 412 | } else { 413 | this.send({ 414 | event: "serviceFailed", 415 | service: "formatter", 416 | error: `Exited with status ${signal || code}`, 417 | }); 418 | } 419 | }); 420 | formatter.proc.on("error", (err) => { 421 | if (!formatter.live) return; 422 | this.send({ 423 | event: "serviceFailed", 424 | service: "formatter", 425 | error: `${err}`, 426 | }); 427 | }); 428 | this.formatter = formatter; 429 | } catch (err) { 430 | this.log(`Error while running code formatter`); 431 | console.log(err); 432 | this.sendError(err); 433 | } 434 | }; 435 | 436 | ensure = async (cmd: string) => { 437 | const code = await this.run(this.privilegedSpawn(bash(cmd)), { 438 | check: false, 439 | }); 440 | this.send({ event: "ensured", code }); 441 | }; 442 | 443 | teardown = async () => { 444 | try { 445 | if (this.tearingDown) { 446 | return; 447 | } 448 | this.log(`Tearing down session`); 449 | this.tearingDown = true; 450 | allSessions.delete(this); 451 | if (this.uidInfo) { 452 | await this.run(this.privilegedTeardown()); 453 | await this.returnUID(); 454 | } 455 | this.ws.terminate(); 456 | } catch (err) { 457 | this.log(`Error during teardown`); 458 | console.log(err); 459 | } 460 | }; 461 | } 462 | -------------------------------------------------------------------------------- /scripts/my_init: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 -u 2 | # -*- coding: utf-8 -*- 3 | # 4 | # From https://github.com/phusion/baseimage-docker/blob/5078b027ba58cce8887acb5c1add0bb8d56f5d38/image/bin/my_init 5 | # Copyright 2013-2015 Phusion Holding B.V. under MIT License 6 | # See https://github.com/phusion/baseimage-docker/blob/5078b027ba58cce8887acb5c1add0bb8d56f5d38/LICENSE.txt 7 | 8 | import argparse 9 | import errno 10 | import json 11 | import os 12 | import os.path 13 | import re 14 | import signal 15 | import stat 16 | import sys 17 | import time 18 | 19 | ENV_INIT_DIRECTORY = os.environ.get('ENV_INIT_DIRECTORY', '/etc/my_init.d') 20 | 21 | KILL_PROCESS_TIMEOUT = int(os.environ.get('KILL_PROCESS_TIMEOUT', 30)) 22 | KILL_ALL_PROCESSES_TIMEOUT = int(os.environ.get('KILL_ALL_PROCESSES_TIMEOUT', 30)) 23 | 24 | LOG_LEVEL_ERROR = 1 25 | LOG_LEVEL_WARN = 1 26 | LOG_LEVEL_INFO = 2 27 | LOG_LEVEL_DEBUG = 3 28 | 29 | SHENV_NAME_WHITELIST_REGEX = re.compile('\W') 30 | 31 | log_level = None 32 | 33 | terminated_child_processes = {} 34 | 35 | _find_unsafe = re.compile(r'[^\w@%+=:,./-]').search 36 | 37 | 38 | class AlarmException(Exception): 39 | pass 40 | 41 | 42 | def error(message): 43 | if log_level >= LOG_LEVEL_ERROR: 44 | sys.stderr.write("*** %s\n" % message) 45 | 46 | 47 | def warn(message): 48 | if log_level >= LOG_LEVEL_WARN: 49 | sys.stderr.write("*** %s\n" % message) 50 | 51 | 52 | def info(message): 53 | if log_level >= LOG_LEVEL_INFO: 54 | sys.stderr.write("*** %s\n" % message) 55 | 56 | 57 | def debug(message): 58 | if log_level >= LOG_LEVEL_DEBUG: 59 | sys.stderr.write("*** %s\n" % message) 60 | 61 | 62 | def ignore_signals_and_raise_keyboard_interrupt(signame): 63 | signal.signal(signal.SIGTERM, signal.SIG_IGN) 64 | signal.signal(signal.SIGINT, signal.SIG_IGN) 65 | raise KeyboardInterrupt(signame) 66 | 67 | 68 | def raise_alarm_exception(): 69 | raise AlarmException('Alarm') 70 | 71 | 72 | def listdir(path): 73 | try: 74 | result = os.stat(path) 75 | except OSError: 76 | return [] 77 | if stat.S_ISDIR(result.st_mode): 78 | return sorted(os.listdir(path)) 79 | else: 80 | return [] 81 | 82 | 83 | def is_exe(path): 84 | try: 85 | return os.path.isfile(path) and os.access(path, os.X_OK) 86 | except OSError: 87 | return False 88 | 89 | 90 | def import_envvars(clear_existing_environment=True, override_existing_environment=True): 91 | if not os.path.exists("/etc/container_environment"): 92 | return 93 | new_env = {} 94 | for envfile in listdir("/etc/container_environment"): 95 | name = os.path.basename(envfile) 96 | with open("/etc/container_environment/" + envfile, "r") as f: 97 | # Text files often end with a trailing newline, which we 98 | # don't want to include in the env variable value. See 99 | # https://github.com/phusion/baseimage-docker/pull/49 100 | value = re.sub('\n\Z', '', f.read()) 101 | new_env[name] = value 102 | if clear_existing_environment: 103 | os.environ.clear() 104 | for name, value in new_env.items(): 105 | if override_existing_environment or name not in os.environ: 106 | os.environ[name] = value 107 | 108 | 109 | def export_envvars(to_dir=True): 110 | if not os.path.exists("/etc/container_environment"): 111 | return 112 | shell_dump = "" 113 | for name, value in os.environ.items(): 114 | if name in ['HOME', 'USER', 'GROUP', 'UID', 'GID', 'SHELL']: 115 | continue 116 | if to_dir: 117 | with open("/etc/container_environment/" + name, "w") as f: 118 | f.write(value) 119 | shell_dump += "export " + sanitize_shenvname(name) + "=" + shquote(value) + "\n" 120 | with open("/etc/container_environment.sh", "w") as f: 121 | f.write(shell_dump) 122 | with open("/etc/container_environment.json", "w") as f: 123 | f.write(json.dumps(dict(os.environ))) 124 | 125 | 126 | def shquote(s): 127 | """Return a shell-escaped version of the string *s*.""" 128 | if not s: 129 | return "''" 130 | if _find_unsafe(s) is None: 131 | return s 132 | 133 | # use single quotes, and put single quotes into double quotes 134 | # the string $'b is then quoted as '$'"'"'b' 135 | return "'" + s.replace("'", "'\"'\"'") + "'" 136 | 137 | 138 | def sanitize_shenvname(s): 139 | """Return string with [0-9a-zA-Z_] characters""" 140 | return re.sub(SHENV_NAME_WHITELIST_REGEX, "_", s) 141 | 142 | 143 | # Waits for the child process with the given PID, while at the same time 144 | # reaping any other child processes that have exited (e.g. adopted child 145 | # processes that have terminated). 146 | 147 | def waitpid_reap_other_children(pid): 148 | global terminated_child_processes 149 | 150 | status = terminated_child_processes.get(pid) 151 | if status: 152 | # A previous call to waitpid_reap_other_children(), 153 | # with an argument not equal to the current argument, 154 | # already waited for this process. Return the status 155 | # that was obtained back then. 156 | del terminated_child_processes[pid] 157 | return status 158 | 159 | done = False 160 | status = None 161 | while not done: 162 | try: 163 | # https://github.com/phusion/baseimage-docker/issues/151#issuecomment-92660569 164 | this_pid, status = os.waitpid(pid, os.WNOHANG) 165 | if this_pid == 0: 166 | this_pid, status = os.waitpid(-1, 0) 167 | if this_pid == pid: 168 | done = True 169 | else: 170 | # Save status for later. 171 | terminated_child_processes[this_pid] = status 172 | except OSError as e: 173 | if e.errno == errno.ECHILD or e.errno == errno.ESRCH: 174 | return None 175 | else: 176 | raise 177 | return status 178 | 179 | 180 | def stop_child_process(name, pid, signo=signal.SIGTERM, time_limit=KILL_PROCESS_TIMEOUT): 181 | info("Shutting down %s (PID %d)..." % (name, pid)) 182 | try: 183 | os.kill(pid, signo) 184 | except OSError: 185 | pass 186 | signal.alarm(time_limit) 187 | try: 188 | try: 189 | waitpid_reap_other_children(pid) 190 | except OSError: 191 | pass 192 | except AlarmException: 193 | warn("%s (PID %d) did not shut down in time. Forcing it to exit." % (name, pid)) 194 | try: 195 | os.kill(pid, signal.SIGKILL) 196 | except OSError: 197 | pass 198 | try: 199 | waitpid_reap_other_children(pid) 200 | except OSError: 201 | pass 202 | finally: 203 | signal.alarm(0) 204 | 205 | 206 | def run_command_killable(*argv): 207 | filename = argv[0] 208 | status = None 209 | pid = os.spawnvp(os.P_NOWAIT, filename, argv) 210 | try: 211 | status = waitpid_reap_other_children(pid) 212 | except BaseException: 213 | warn("An error occurred. Aborting.") 214 | stop_child_process(filename, pid) 215 | raise 216 | if status != 0: 217 | if status is None: 218 | error("%s exited with unknown status\n" % filename) 219 | else: 220 | error("%s failed with status %d\n" % (filename, os.WEXITSTATUS(status))) 221 | sys.exit(1) 222 | 223 | 224 | def run_command_killable_and_import_envvars(*argv): 225 | run_command_killable(*argv) 226 | import_envvars() 227 | export_envvars(False) 228 | 229 | 230 | def kill_all_processes(time_limit): 231 | info("Killing all processes...") 232 | try: 233 | os.kill(-1, signal.SIGTERM) 234 | except OSError: 235 | pass 236 | signal.alarm(time_limit) 237 | try: 238 | # Wait until no more child processes exist. 239 | done = False 240 | while not done: 241 | try: 242 | os.waitpid(-1, 0) 243 | except OSError as e: 244 | if e.errno == errno.ECHILD: 245 | done = True 246 | else: 247 | raise 248 | except AlarmException: 249 | warn("Not all processes have exited in time. Forcing them to exit.") 250 | try: 251 | os.kill(-1, signal.SIGKILL) 252 | except OSError: 253 | pass 254 | finally: 255 | signal.alarm(0) 256 | 257 | 258 | def run_startup_files(): 259 | # Run ENV_INIT_DIRECTORY/* 260 | for name in listdir(ENV_INIT_DIRECTORY): 261 | filename = os.path.join(ENV_INIT_DIRECTORY, name) 262 | if is_exe(filename): 263 | info("Running %s..." % filename) 264 | run_command_killable_and_import_envvars(filename) 265 | 266 | # Run /etc/rc.local. 267 | if is_exe("/etc/rc.local"): 268 | info("Running /etc/rc.local...") 269 | run_command_killable_and_import_envvars("/etc/rc.local") 270 | 271 | 272 | def run_pre_shutdown_scripts(): 273 | debug("Running pre-shutdown scripts...") 274 | 275 | # Run /etc/my_init.pre_shutdown.d/* 276 | for name in listdir("/etc/my_init.pre_shutdown.d"): 277 | filename = "/etc/my_init.pre_shutdown.d/" + name 278 | if is_exe(filename): 279 | info("Running %s..." % filename) 280 | run_command_killable(filename) 281 | 282 | 283 | def run_post_shutdown_scripts(): 284 | debug("Running post-shutdown scripts...") 285 | 286 | # Run /etc/my_init.post_shutdown.d/* 287 | for name in listdir("/etc/my_init.post_shutdown.d"): 288 | filename = "/etc/my_init.post_shutdown.d/" + name 289 | if is_exe(filename): 290 | info("Running %s..." % filename) 291 | run_command_killable(filename) 292 | 293 | 294 | def start_runit(): 295 | info("Booting runit daemon...") 296 | pid = os.spawnl(os.P_NOWAIT, "/usr/bin/runsvdir", "/usr/bin/runsvdir", 297 | "-P", "/etc/service") 298 | info("Runit started as PID %d" % pid) 299 | return pid 300 | 301 | 302 | def wait_for_runit_or_interrupt(pid): 303 | status = waitpid_reap_other_children(pid) 304 | return (True, status) 305 | 306 | 307 | def shutdown_runit_services(quiet=False): 308 | if not quiet: 309 | debug("Begin shutting down runit services...") 310 | os.system("/usr/bin/sv -w %d force-stop /etc/service/* > /dev/null" % KILL_PROCESS_TIMEOUT) 311 | 312 | 313 | def wait_for_runit_services(): 314 | debug("Waiting for runit services to exit...") 315 | done = False 316 | while not done: 317 | done = os.system("/usr/bin/sv status /etc/service/* | grep -q '^run:'") != 0 318 | if not done: 319 | time.sleep(0.1) 320 | # According to https://github.com/phusion/baseimage-docker/issues/315 321 | # there is a bug or race condition in Runit, causing it 322 | # not to shutdown services that are already being started. 323 | # So during shutdown we repeatedly instruct Runit to shutdown 324 | # services. 325 | shutdown_runit_services(True) 326 | 327 | 328 | def install_insecure_key(): 329 | info("Installing insecure SSH key for user root") 330 | run_command_killable("/usr/sbin/enable_insecure_key") 331 | 332 | 333 | def main(args): 334 | import_envvars(False, False) 335 | export_envvars() 336 | 337 | if args.enable_insecure_key: 338 | install_insecure_key() 339 | 340 | if not args.skip_startup_files: 341 | run_startup_files() 342 | 343 | runit_exited = False 344 | exit_code = None 345 | 346 | if not args.skip_runit: 347 | runit_pid = start_runit() 348 | try: 349 | exit_status = None 350 | if len(args.main_command) == 0: 351 | runit_exited, exit_code = wait_for_runit_or_interrupt(runit_pid) 352 | if runit_exited: 353 | if exit_code is None: 354 | info("Runit exited with unknown status") 355 | exit_status = 1 356 | else: 357 | exit_status = os.WEXITSTATUS(exit_code) 358 | info("Runit exited with status %d" % exit_status) 359 | else: 360 | info("Running %s..." % " ".join(args.main_command)) 361 | pid = os.spawnvp(os.P_NOWAIT, args.main_command[0], args.main_command) 362 | try: 363 | exit_code = waitpid_reap_other_children(pid) 364 | if exit_code is None: 365 | info("%s exited with unknown status." % args.main_command[0]) 366 | exit_status = 1 367 | else: 368 | exit_status = os.WEXITSTATUS(exit_code) 369 | info("%s exited with status %d." % (args.main_command[0], exit_status)) 370 | except KeyboardInterrupt: 371 | stop_child_process(args.main_command[0], pid) 372 | raise 373 | except BaseException: 374 | warn("An error occurred. Aborting.") 375 | stop_child_process(args.main_command[0], pid) 376 | raise 377 | sys.exit(exit_status) 378 | finally: 379 | if not args.skip_runit: 380 | run_pre_shutdown_scripts() 381 | shutdown_runit_services() 382 | if not runit_exited: 383 | stop_child_process("runit daemon", runit_pid) 384 | wait_for_runit_services() 385 | run_post_shutdown_scripts() 386 | 387 | # Parse options. 388 | parser = argparse.ArgumentParser(description='Initialize the system.') 389 | parser.add_argument('main_command', metavar='MAIN_COMMAND', type=str, nargs='*', 390 | help='The main command to run. (default: runit)') 391 | parser.add_argument('--enable-insecure-key', dest='enable_insecure_key', 392 | action='store_const', const=True, default=False, 393 | help='Install the insecure SSH key') 394 | parser.add_argument('--skip-startup-files', dest='skip_startup_files', 395 | action='store_const', const=True, default=False, 396 | help='Skip running /etc/my_init.d/* and /etc/rc.local') 397 | parser.add_argument('--skip-runit', dest='skip_runit', 398 | action='store_const', const=True, default=False, 399 | help='Do not run runit services') 400 | parser.add_argument('--no-kill-all-on-exit', dest='kill_all_on_exit', 401 | action='store_const', const=False, default=True, 402 | help='Don\'t kill all processes on the system upon exiting') 403 | parser.add_argument('--quiet', dest='log_level', 404 | action='store_const', const=LOG_LEVEL_WARN, default=LOG_LEVEL_INFO, 405 | help='Only print warnings and errors') 406 | args = parser.parse_args() 407 | log_level = args.log_level 408 | 409 | if args.skip_runit and len(args.main_command) == 0: 410 | error("When --skip-runit is given, you must also pass a main command.") 411 | sys.exit(1) 412 | 413 | # Run main function. 414 | signal.signal(signal.SIGTERM, lambda signum, frame: ignore_signals_and_raise_keyboard_interrupt('SIGTERM')) 415 | signal.signal(signal.SIGINT, lambda signum, frame: ignore_signals_and_raise_keyboard_interrupt('SIGINT')) 416 | signal.signal(signal.SIGALRM, lambda signum, frame: raise_alarm_exception()) 417 | try: 418 | main(args) 419 | except KeyboardInterrupt: 420 | warn("Init system aborted.") 421 | exit(2) 422 | finally: 423 | if args.kill_all_on_exit: 424 | kill_all_processes(KILL_ALL_PROCESSES_TIMEOUT) 425 | -------------------------------------------------------------------------------- /backend/src/test-runner.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as process from "process"; 3 | import { promisify } from "util"; 4 | 5 | import * as _ from "lodash"; 6 | import { Moment } from "moment"; 7 | import * as moment from "moment"; 8 | import PQueue from "p-queue"; 9 | import * as rimraf from "rimraf"; 10 | import { v4 as getUUID } from "uuid"; 11 | 12 | import * as api from "./api"; 13 | import { LangConfig, langs } from "./langs"; 14 | 15 | function parseIntOr(thing: any, def: number) { 16 | const num = parseInt(thing); 17 | return Number.isNaN(num) ? def : num; 18 | } 19 | 20 | const TIMEOUT_FACTOR = parseIntOr(process.env.TIMEOUT_FACTOR, 1); 21 | const CONCURRENCY = parseIntOr(process.env.CONCURRENCY, 2); 22 | const BASE_TIMEOUT_SECS = 5; 23 | 24 | function findPosition(str: string, idx: number) { 25 | const lines = str.substring(0, idx).split("\n"); 26 | const line = lines.length - 1; 27 | const character = lines[lines.length - 1].length; 28 | return { line, character }; 29 | } 30 | 31 | async function sendInput(send: (msg: any) => any, input: string) { 32 | for (const line of input.split("\n")) { 33 | if (line.startsWith("DELAY:")) { 34 | const delay = parseFloat(line.replace(/DELAY: */, "")); 35 | if (Number.isNaN(delay)) continue; 36 | await new Promise((resolve) => 37 | setTimeout(resolve, delay * 1000 * TIMEOUT_FACTOR) 38 | ); 39 | } else { 40 | send({ event: "terminalInput", input: line + "\r" }); 41 | } 42 | } 43 | } 44 | 45 | class Test { 46 | lang: string; 47 | type: string; 48 | messages: any[] = []; 49 | timedOut: boolean = false; 50 | handledMessages: number = 0; 51 | handleUpdate: () => void = () => {}; 52 | startTime: Moment | null = null; 53 | 54 | get config() { 55 | return langs[this.lang]; 56 | } 57 | 58 | ws: any = null; 59 | 60 | record = (msg: any) => { 61 | const dur = moment.duration(moment().diff(this.startTime!)); 62 | this.messages.push({ time: dur.asSeconds(), ...msg }); 63 | }; 64 | 65 | send = (msg: any) => { 66 | this.ws.onMessage(JSON.stringify(msg)); 67 | this.record(msg); 68 | this.handledMessages += 1; 69 | }; 70 | 71 | constructor(lang: string, type: string) { 72 | this.lang = lang; 73 | this.type = type; 74 | } 75 | 76 | getLog = (opts?: any) => { 77 | opts = opts || {}; 78 | return this.messages 79 | .map((msg: any) => JSON.stringify(msg, null, opts.pretty && 2)) 80 | .join("\n"); 81 | }; 82 | 83 | run = async () => { 84 | if ((this.config.skip || []).includes(this.type)) { 85 | return "skipped"; 86 | } 87 | this.startTime = moment(); 88 | let session = null; 89 | let timeout = null; 90 | try { 91 | const that = this; 92 | this.ws = { 93 | on: function (type: string, handler: any) { 94 | switch (type) { 95 | case "message": 96 | this.onMessage = handler; 97 | for (const msg of this.messageQueue) { 98 | this.onMessage(msg); 99 | } 100 | this.messageQueue = []; 101 | break; 102 | case "close": 103 | case "error": 104 | // No need to clean up, we'll call teardown() explicitly. 105 | break; 106 | default: 107 | throw new Error(`unexpected websocket handler type: ${type}`); 108 | } 109 | }, 110 | onMessage: function (msg: any) { 111 | this.messageQueue.push(msg); 112 | }, 113 | messageQueue: [] as any[], 114 | send: function (data: string) { 115 | that.record(JSON.parse(data)); 116 | that.handleUpdate(); 117 | }, 118 | terminate: function () {}, 119 | }; 120 | session = new api.Session(this.ws, this.lang, (msg: string) => { 121 | this.record({ event: "serverLog", message: msg }); 122 | }); 123 | timeout = setTimeout(() => { 124 | this.timedOut = true; 125 | this.handleUpdate(); 126 | }, (this.config.timeout || BASE_TIMEOUT_SECS) * 1000 * TIMEOUT_FACTOR); 127 | await session.setup(); 128 | switch (this.type) { 129 | case "ensure": 130 | await this.testEnsure(); 131 | break; 132 | case "run": 133 | await this.testRun(); 134 | break; 135 | case "repl": 136 | await this.testRepl(); 137 | break; 138 | case "runrepl": 139 | await this.testRunRepl(); 140 | break; 141 | case "scope": 142 | await this.testScope(); 143 | break; 144 | case "format": 145 | await this.testFormat(); 146 | break; 147 | case "lsp": 148 | await this.testLsp(); 149 | break; 150 | default: 151 | throw new Error(`Unexpected test type: ${this.type}`); 152 | } 153 | } finally { 154 | this.ws = null; 155 | if (timeout) { 156 | clearTimeout(timeout); 157 | } 158 | if (session) { 159 | await session.teardown(); 160 | } 161 | } 162 | }; 163 | 164 | wait = async (desc: string, handler: (msg: any) => T) => { 165 | return await new Promise((resolve, reject) => { 166 | this.handleUpdate = () => { 167 | if (this.timedOut) { 168 | reject(new Error(`timeout while waiting for ${desc}`)); 169 | } else { 170 | while (this.handledMessages < this.messages.length) { 171 | const msg = this.messages[this.handledMessages]; 172 | const result = handler(msg); 173 | if (![undefined, null, false].includes(result as any)) { 174 | resolve(result); 175 | } 176 | this.handledMessages += 1; 177 | } 178 | } 179 | }; 180 | this.handleUpdate(); 181 | }); 182 | }; 183 | 184 | waitForOutput = async (pattern: string, maxLength?: number) => { 185 | let output = ""; 186 | return await this.wait(`output ${JSON.stringify(pattern)}`, (msg: any) => { 187 | const prevLength = output.length; 188 | if (msg.event === "terminalOutput") { 189 | output += msg.output; 190 | } 191 | if (typeof maxLength === "number") { 192 | return ( 193 | output 194 | .substring(prevLength - maxLength) 195 | .match(new RegExp(pattern)) !== null 196 | ); 197 | } else { 198 | return output.indexOf(pattern, prevLength - pattern.length) != -1; 199 | } 200 | }); 201 | }; 202 | 203 | testEnsure = async () => { 204 | this.send({ event: "ensure" }); 205 | const code = await this.wait("ensure response", (msg: any) => { 206 | if (msg.event === "ensured") { 207 | return msg.code; 208 | } 209 | }); 210 | if (code !== 0) { 211 | throw new Error(`ensure failed with code ${code}`); 212 | } 213 | }; 214 | testRun = async () => { 215 | const pattern = this.config.hello || "Hello, world!"; 216 | this.send({ event: "runCode", code: this.config.template }); 217 | if (this.config.helloInput !== undefined) { 218 | sendInput(this.send, this.config.helloInput); 219 | } 220 | await this.waitForOutput(pattern, this.config.helloMaxLength); 221 | }; 222 | testRepl = async () => { 223 | const input = this.config.input || "123 * 234"; 224 | const output = this.config.output || "28782"; 225 | sendInput(this.send, input); 226 | await this.waitForOutput(output); 227 | }; 228 | testRunRepl = async () => { 229 | const input = this.config.runReplInput || this.config.input || "123 * 234"; 230 | const output = this.config.runReplOutput || this.config.output || "28782"; 231 | this.send({ event: "runCode", code: this.config.template }); 232 | sendInput(this.send, input); 233 | await this.waitForOutput(output); 234 | }; 235 | testScope = async () => { 236 | const code = this.config.scope!.code; 237 | const after = this.config.scope!.after; 238 | const input = this.config.scope!.input || "x"; 239 | const output = this.config.scope!.output || "28782"; 240 | let allCode = this.config.template; 241 | if (!allCode.endsWith("\n")) { 242 | allCode += "\n"; 243 | } 244 | if (after) { 245 | allCode = allCode.replace(after + "\n", after + "\n" + code + "\n"); 246 | } else { 247 | allCode = allCode + code + "\n"; 248 | } 249 | this.send({ event: "runCode", code: allCode }); 250 | sendInput(this.send, input); 251 | await this.waitForOutput(output); 252 | }; 253 | testFormat = async () => { 254 | const input = this.config.format!.input; 255 | const output = this.config.format!.output || this.config.template; 256 | this.send({ event: "formatCode", code: input }); 257 | const result = await this.wait("formatter response", (msg: any) => { 258 | if (msg.event === "formattedCode") { 259 | return msg.code; 260 | } 261 | }); 262 | if (output !== result) { 263 | throw new Error("formatted code did not match"); 264 | } 265 | }; 266 | testLsp = async () => { 267 | const insertedCode = this.config.lsp!.code!; 268 | const after = this.config.lsp!.after; 269 | const item = this.config.lsp!.item!; 270 | const idx = after 271 | ? this.config.template.indexOf(after) + after.length 272 | : this.config.template.length; 273 | const code = 274 | this.config.template.slice(0, idx) + 275 | insertedCode + 276 | this.config.template.slice(idx); 277 | const root = await this.wait("lspStarted message", (msg: any) => { 278 | if (msg.event === "lspStarted") { 279 | return msg.root; 280 | } 281 | }); 282 | this.send({ 283 | event: "lspInput", 284 | input: { 285 | jsonrpc: "2.0", 286 | id: "0d75333a-47d8-4da8-8030-c81d7bd9eed7", 287 | method: "initialize", 288 | params: { 289 | processId: null, 290 | clientInfo: { name: "vscode" }, 291 | rootPath: root, 292 | rootUri: `file://${root}`, 293 | capabilities: { 294 | workspace: { 295 | applyEdit: true, 296 | workspaceEdit: { 297 | documentChanges: true, 298 | resourceOperations: ["create", "rename", "delete"], 299 | failureHandling: "textOnlyTransactional", 300 | }, 301 | didChangeConfiguration: { dynamicRegistration: true }, 302 | didChangeWatchedFiles: { dynamicRegistration: true }, 303 | symbol: { 304 | dynamicRegistration: true, 305 | symbolKind: { 306 | valueSet: [ 307 | 1, 308 | 2, 309 | 3, 310 | 4, 311 | 5, 312 | 6, 313 | 7, 314 | 8, 315 | 9, 316 | 10, 317 | 11, 318 | 12, 319 | 13, 320 | 14, 321 | 15, 322 | 16, 323 | 17, 324 | 18, 325 | 19, 326 | 20, 327 | 21, 328 | 22, 329 | 23, 330 | 24, 331 | 25, 332 | 26, 333 | ], 334 | }, 335 | }, 336 | executeCommand: { dynamicRegistration: true }, 337 | configuration: true, 338 | workspaceFolders: true, 339 | }, 340 | textDocument: { 341 | publishDiagnostics: { 342 | relatedInformation: true, 343 | versionSupport: false, 344 | tagSupport: { valueSet: [1, 2] }, 345 | }, 346 | synchronization: { 347 | dynamicRegistration: true, 348 | willSave: true, 349 | willSaveWaitUntil: true, 350 | didSave: true, 351 | }, 352 | completion: { 353 | dynamicRegistration: true, 354 | contextSupport: true, 355 | completionItem: { 356 | snippetSupport: true, 357 | commitCharactersSupport: true, 358 | documentationFormat: ["markdown", "plaintext"], 359 | deprecatedSupport: true, 360 | preselectSupport: true, 361 | tagSupport: { valueSet: [1] }, 362 | }, 363 | completionItemKind: { 364 | valueSet: [ 365 | 1, 366 | 2, 367 | 3, 368 | 4, 369 | 5, 370 | 6, 371 | 7, 372 | 8, 373 | 9, 374 | 10, 375 | 11, 376 | 12, 377 | 13, 378 | 14, 379 | 15, 380 | 16, 381 | 17, 382 | 18, 383 | 19, 384 | 20, 385 | 21, 386 | 22, 387 | 23, 388 | 24, 389 | 25, 390 | ], 391 | }, 392 | }, 393 | hover: { 394 | dynamicRegistration: true, 395 | contentFormat: ["markdown", "plaintext"], 396 | }, 397 | signatureHelp: { 398 | dynamicRegistration: true, 399 | signatureInformation: { 400 | documentationFormat: ["markdown", "plaintext"], 401 | parameterInformation: { labelOffsetSupport: true }, 402 | }, 403 | contextSupport: true, 404 | }, 405 | definition: { dynamicRegistration: true, linkSupport: true }, 406 | references: { dynamicRegistration: true }, 407 | documentHighlight: { dynamicRegistration: true }, 408 | documentSymbol: { 409 | dynamicRegistration: true, 410 | symbolKind: { 411 | valueSet: [ 412 | 1, 413 | 2, 414 | 3, 415 | 4, 416 | 5, 417 | 6, 418 | 7, 419 | 8, 420 | 9, 421 | 10, 422 | 11, 423 | 12, 424 | 13, 425 | 14, 426 | 15, 427 | 16, 428 | 17, 429 | 18, 430 | 19, 431 | 20, 432 | 21, 433 | 22, 434 | 23, 435 | 24, 436 | 25, 437 | 26, 438 | ], 439 | }, 440 | hierarchicalDocumentSymbolSupport: true, 441 | }, 442 | codeAction: { 443 | dynamicRegistration: true, 444 | isPreferredSupport: true, 445 | codeActionLiteralSupport: { 446 | codeActionKind: { 447 | valueSet: [ 448 | "", 449 | "quickfix", 450 | "refactor", 451 | "refactor.extract", 452 | "refactor.inline", 453 | "refactor.rewrite", 454 | "source", 455 | "source.organizeImports", 456 | ], 457 | }, 458 | }, 459 | }, 460 | codeLens: { dynamicRegistration: true }, 461 | formatting: { dynamicRegistration: true }, 462 | rangeFormatting: { dynamicRegistration: true }, 463 | onTypeFormatting: { dynamicRegistration: true }, 464 | rename: { dynamicRegistration: true, prepareSupport: true }, 465 | documentLink: { dynamicRegistration: true, tooltipSupport: true }, 466 | typeDefinition: { dynamicRegistration: true, linkSupport: true }, 467 | implementation: { dynamicRegistration: true, linkSupport: true }, 468 | colorProvider: { dynamicRegistration: true }, 469 | foldingRange: { 470 | dynamicRegistration: true, 471 | rangeLimit: 5000, 472 | lineFoldingOnly: true, 473 | }, 474 | declaration: { dynamicRegistration: true, linkSupport: true }, 475 | }, 476 | }, 477 | initializationOptions: this.config.lsp!.init || {}, 478 | trace: "off", 479 | workspaceFolders: [ 480 | { 481 | uri: `file://${root}`, 482 | name: `file://${root}`, 483 | }, 484 | ], 485 | }, 486 | }, 487 | }); 488 | await this.wait("response to lsp initialize", (msg: any) => { 489 | return ( 490 | msg.event === "lspOutput" && 491 | msg.output.id === "0d75333a-47d8-4da8-8030-c81d7bd9eed7" 492 | ); 493 | }); 494 | this.send({ 495 | event: "lspInput", 496 | input: { jsonrpc: "2.0", method: "initialized", params: {} }, 497 | }); 498 | this.send({ 499 | event: "lspInput", 500 | input: { 501 | jsonrpc: "2.0", 502 | method: "textDocument/didOpen", 503 | params: { 504 | textDocument: { 505 | uri: `file://${root}/${this.config.main}`, 506 | languageId: 507 | this.config.lsp!.lang || this.config.monacoLang || "plaintext", 508 | version: 1, 509 | text: code, 510 | }, 511 | }, 512 | }, 513 | }); 514 | this.send({ 515 | event: "lspInput", 516 | input: { 517 | jsonrpc: "2.0", 518 | id: "ecdb8a55-f755-4553-ae8e-91d6ebbc2045", 519 | method: "textDocument/completion", 520 | params: { 521 | textDocument: { 522 | uri: `file://${root}/${this.config.main}`, 523 | }, 524 | position: findPosition(code, idx + insertedCode.length), 525 | context: { triggerKind: 1 }, 526 | }, 527 | }, 528 | }); 529 | const items: any = await this.wait( 530 | "response to lsp completion request", 531 | (msg: any) => { 532 | if (msg.event === "lspOutput") { 533 | if (msg.output.method === "workspace/configuration") { 534 | this.send({ 535 | event: "lspInput", 536 | input: { 537 | jsonrpc: "2.0", 538 | id: msg.output.id, 539 | result: Array(msg.output.params.items.length).fill( 540 | this.config.lsp!.config !== undefined 541 | ? this.config.lsp!.config 542 | : {} 543 | ), 544 | }, 545 | }); 546 | } else if (msg.output.id === "ecdb8a55-f755-4553-ae8e-91d6ebbc2045") { 547 | return msg.output.result.items || msg.output.result; 548 | } 549 | } 550 | } 551 | ); 552 | if ( 553 | !(items && items.filter(({ label }: any) => label === item).length > 0) 554 | ) { 555 | throw new Error("completion item did not appear"); 556 | } 557 | }; 558 | } 559 | 560 | function lint(lang: string) { 561 | const config = langs[lang]; 562 | if (!config.template.endsWith("\n")) { 563 | throw new Error("template is missing a trailing newline"); 564 | } 565 | // These can be removed when the types are adjusted to make these 566 | // situations impossible. 567 | if ( 568 | config.format && 569 | !config.format.input && 570 | !(config.skip || []).includes("format") 571 | ) { 572 | throw new Error("formatter is missing test"); 573 | } 574 | if ( 575 | config.lsp && 576 | !(config.lsp.code && config.lsp.item) && 577 | !(config.skip || []).includes("lsp") 578 | ) { 579 | throw new Error("LSP is missing test"); 580 | } 581 | } 582 | 583 | const testTypes: { 584 | [key: string]: { 585 | pred: (cfg: LangConfig) => boolean; 586 | }; 587 | } = { 588 | ensure: { 589 | pred: ({ ensure }) => (ensure ? true : false), 590 | }, 591 | run: { pred: (config) => true }, 592 | repl: { 593 | pred: ({ repl }) => (repl ? true : false), 594 | }, 595 | runrepl: { 596 | pred: ({ repl }) => (repl ? true : false), 597 | }, 598 | scope: { 599 | pred: ({ scope }) => (scope ? true : false), 600 | }, 601 | format: { 602 | pred: ({ format }) => (format ? true : false), 603 | }, 604 | lsp: { pred: ({ lsp }) => (lsp && lsp.code ? true : false) }, 605 | }; 606 | 607 | function getTestList() { 608 | const tests: { lang: string; type: string }[] = []; 609 | for (const [id, cfg] of Object.entries(langs)) { 610 | for (const [type, { pred }] of Object.entries(testTypes)) { 611 | if (pred(cfg)) { 612 | tests.push({ lang: id, type }); 613 | } 614 | } 615 | } 616 | return tests; 617 | } 618 | 619 | async function writeLog( 620 | lang: string, 621 | type: string, 622 | result: string, 623 | log: string 624 | ) { 625 | log = `${result.toUpperCase()}: ${lang}/${type}\n` + log; 626 | await promisify(fs.mkdir)(`tests/${lang}`, { recursive: true }); 627 | await promisify(fs.writeFile)(`tests/${lang}/${type}.log`, log); 628 | await promisify(fs.mkdir)(`tests-run/${lang}`, { recursive: true }); 629 | await promisify(fs.symlink)( 630 | `../../tests/${lang}/${type}.log`, 631 | `tests-run/${lang}/${type}.log` 632 | ); 633 | await promisify(fs.mkdir)(`tests-${result}/${lang}`, { recursive: true }); 634 | await promisify(fs.symlink)( 635 | `../../tests/${lang}/${type}.log`, 636 | `tests-${result}/${lang}/${type}.log` 637 | ); 638 | } 639 | 640 | async function main() { 641 | let tests = getTestList(); 642 | const args = process.argv.slice(2); 643 | for (const arg of args) { 644 | tests = tests.filter( 645 | ({ lang, type }) => 646 | arg 647 | .split(",") 648 | .filter((arg) => 649 | [lang, type].concat(langs[lang].aliases || []).includes(arg) 650 | ).length > 0 651 | ); 652 | } 653 | if (tests.length === 0) { 654 | console.error("no tests selected"); 655 | process.exit(1); 656 | } 657 | console.error(`Running ${tests.length} test${tests.length !== 1 ? "s" : ""}`); 658 | const lintSeen = new Set(); 659 | let lintPassed = new Set(); 660 | let lintFailed = new Map(); 661 | for (const { lang } of tests) { 662 | if (!lintSeen.has(lang)) { 663 | lintSeen.add(lang); 664 | try { 665 | lint(lang); 666 | lintPassed.add(lang); 667 | } catch (err) { 668 | lintFailed.set(lang, err); 669 | } 670 | } 671 | } 672 | if (lintFailed.size > 0) { 673 | console.error( 674 | `Language${lintFailed.size !== 1 ? "s" : ""} failed linting:` 675 | ); 676 | console.error( 677 | Array.from(lintFailed) 678 | .map(([lang, err]) => ` - ${lang} (${err})`) 679 | .join("\n") 680 | ); 681 | process.exit(1); 682 | } 683 | await promisify(rimraf)("tests-run"); 684 | await promisify(rimraf)("tests-passed"); 685 | await promisify(rimraf)("tests-skipped"); 686 | await promisify(rimraf)("tests-failed"); 687 | const queue = new PQueue({ concurrency: CONCURRENCY }); 688 | let passed = new Set(); 689 | let skipped = new Set(); 690 | let failed = new Map(); 691 | for (const { lang, type } of tests) { 692 | queue.add(async () => { 693 | const test = new Test(lang, type); 694 | let err; 695 | try { 696 | err = await test.run(); 697 | } catch (error) { 698 | err = error; 699 | } 700 | if (err === "skipped") { 701 | skipped.add({ lang, type }); 702 | console.error(`SKIPPED: ${lang}/${type}`); 703 | await writeLog(lang, type, "skipped", ""); 704 | } else if (!err) { 705 | passed.add({ lang, type }); 706 | console.error(`PASSED: ${lang}/${type}`); 707 | await writeLog( 708 | lang, 709 | type, 710 | "passed", 711 | test.getLog({ pretty: true }) + "\n" 712 | ); 713 | } else { 714 | failed.set({ lang, type }, err); 715 | console.error(`FAILED: ${lang}/${type}`); 716 | console.error(test.getLog()); 717 | console.error(err); 718 | await writeLog( 719 | lang, 720 | type, 721 | "failed", 722 | test.getLog({ pretty: true }) + 723 | "\n" + 724 | (err.stack ? err.stack + "\n" : err ? `${err}` : "") 725 | ); 726 | } 727 | }); 728 | } 729 | await queue.onIdle(); 730 | console.error(); 731 | console.error( 732 | "================================================================================" 733 | ); 734 | console.error(); 735 | if (passed.size > 0) { 736 | console.error(`${passed.size} test${passed.size !== 1 ? "s" : ""} PASSED`); 737 | } 738 | if (skipped.size > 0) { 739 | console.error( 740 | `${skipped.size} test${skipped.size !== 1 ? "s" : ""} SKIPPED` 741 | ); 742 | } 743 | if (failed.size > 0) { 744 | console.error(`${failed.size} test${failed.size !== 1 ? "s" : ""} FAILED`); 745 | _.sortBy(Array.from(failed), [ 746 | ([{ lang }, _]: any) => lang, 747 | ([{ type }, _]: any) => type, 748 | ]).forEach(([{ lang, type }, err]) => 749 | console.error(` - ${lang}/${type} (${err})`) 750 | ); 751 | } 752 | process.exit(failed.size > 0 ? 1 : 0); 753 | } 754 | 755 | main().catch(console.error); 756 | --------------------------------------------------------------------------------