├── .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 |
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 | [](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 |
--------------------------------------------------------------------------------