Compare commits
346 commits
from_cheer
...
main
Author | SHA1 | Date | |
---|---|---|---|
![]() |
239376dcef | ||
![]() |
16714160aa | ||
![]() |
52dcded70b | ||
![]() |
fb4117d844 | ||
![]() |
d635622d6f | ||
![]() |
74fbbc2209 | ||
![]() |
3e42875cb9 | ||
![]() |
8d473a0225 | ||
![]() |
2222951621 | ||
![]() |
aee895cabb | ||
![]() |
78be06afce | ||
![]() |
ce162c9e8c | ||
![]() |
d0e6d0e9ea | ||
![]() |
9a5f77d4b7 | ||
![]() |
cdd095e776 | ||
![]() |
cd75685862 | ||
![]() |
b33e3a4356 | ||
![]() |
004ea4f264 | ||
![]() |
00b5a3f42b | ||
![]() |
34f2a564db | ||
![]() |
46ffec65e2 | ||
![]() |
5716055716 | ||
![]() |
28f85baf59 | ||
![]() |
59740754e8 | ||
![]() |
529f720ae1 | ||
![]() |
71715e5040 | ||
![]() |
46f21f3a12 | ||
![]() |
f3cf5750ab | ||
![]() |
a874b2a332 | ||
![]() |
310a9ce2e2 | ||
![]() |
7bf9990995 | ||
![]() |
e28cf214df | ||
![]() |
9b723dcb8a | ||
![]() |
8055466b7a | ||
![]() |
aa1935e389 | ||
![]() |
51b63329ab | ||
![]() |
0a5ceaf9ea | ||
![]() |
236863a0b0 | ||
![]() |
4fc2577819 | ||
![]() |
88bea224d7 | ||
![]() |
3d56bc2182 | ||
![]() |
56b40dbef8 | ||
![]() |
517a5997fb | ||
![]() |
0f9381c00b | ||
![]() |
e01ffeb3db | ||
![]() |
a26b523053 | ||
![]() |
14621dbec2 | ||
![]() |
a3fc89faeb | ||
![]() |
20533a8c35 | ||
![]() |
32095987de | ||
![]() |
0069c378a7 | ||
![]() |
f64bebfe40 | ||
![]() |
9d841e48a4 | ||
![]() |
2f4a22a659 | ||
![]() |
26239de119 | ||
![]() |
98ab63f72c | ||
![]() |
43992f0864 | ||
![]() |
e0e2fca2a0 | ||
![]() |
86d4477e1c | ||
![]() |
e4b9b50072 | ||
![]() |
fcf626d03b | ||
![]() |
28afdc35ef | ||
![]() |
9858b64752 | ||
![]() |
cb1f2f7fc3 | ||
![]() |
307669f7c4 | ||
![]() |
0f30d2273a | ||
![]() |
b1956d3af8 | ||
![]() |
98a0c2a47b | ||
![]() |
d4db6f8e16 | ||
![]() |
208cfa8e0d | ||
![]() |
d28c611806 | ||
![]() |
9962e2ce43 | ||
![]() |
8adc03ac8f | ||
![]() |
97fb17dfe5 | ||
![]() |
96805eca37 | ||
![]() |
ba68b6fe02 | ||
![]() |
930fc96242 | ||
![]() |
6ca6cf58a0 | ||
![]() |
201636aae7 | ||
![]() |
1e407e1b45 | ||
![]() |
1ecc59dcd5 | ||
![]() |
12ed378cc2 | ||
![]() |
7172350071 | ||
![]() |
f3d0ab6fb3 | ||
![]() |
208fee9353 | ||
![]() |
0151a0fd16 | ||
![]() |
1ee73b270a | ||
![]() |
5bcdeffb84 | ||
![]() |
2b01b09aab | ||
![]() |
9b9e8ffe44 | ||
![]() |
ecbea46e41 | ||
![]() |
b2ae6881d6 | ||
![]() |
000f925216 | ||
![]() |
ccbaf8749d | ||
![]() |
70c748a62a | ||
![]() |
422d345904 | ||
![]() |
e39a820f32 | ||
![]() |
f6b931c273 | ||
![]() |
7ab443c30c | ||
![]() |
326cc40921 | ||
![]() |
60b8f103b6 | ||
![]() |
c84a38ca02 | ||
![]() |
f7fc244db4 | ||
![]() |
bb46f1340e | ||
![]() |
d4fd5f1be5 | ||
![]() |
b1e87af6f2 | ||
![]() |
e2589d8179 | ||
![]() |
0a9a044b27 | ||
![]() |
834566aa0e | ||
![]() |
5f2fe65fe7 | ||
![]() |
f88f568f7e | ||
![]() |
134a947547 | ||
![]() |
854b93ec07 | ||
![]() |
fe7ab83fdd | ||
![]() |
12b3b3f89c | ||
![]() |
ff2486d6e2 | ||
![]() |
9de48becfa | ||
![]() |
a43f439179 | ||
![]() |
f4648b08e6 | ||
![]() |
de34cb587e | ||
![]() |
113ef58d50 | ||
![]() |
96ddd4b2de | ||
![]() |
fc64f9f987 | ||
![]() |
f1ef46cda1 | ||
![]() |
0d60f79c99 | ||
![]() |
a21802e25e | ||
![]() |
98afe6dd00 | ||
![]() |
ad7489d869 | ||
![]() |
ed52c12a83 | ||
![]() |
a838ffc97a | ||
![]() |
df5bbfa0e1 | ||
![]() |
73f9e77a17 | ||
![]() |
a7c4bc573c | ||
![]() |
95c7a857b9 | ||
![]() |
56257e6f69 | ||
![]() |
f586c4bcf2 | ||
![]() |
74e18f2b38 | ||
![]() |
92edbb7b96 | ||
![]() |
274931dc12 | ||
![]() |
4f13791f71 | ||
![]() |
fdf57ef9b0 | ||
![]() |
978187d61b | ||
![]() |
5094953eb2 | ||
![]() |
05c234a528 | ||
![]() |
06df68fdea | ||
![]() |
53413d087b | ||
![]() |
1760888da2 | ||
![]() |
bffe17fd69 | ||
![]() |
f2fb54c29f | ||
![]() |
9a2dcb0c8f | ||
![]() |
20e7cdc5db | ||
![]() |
43e10289c9 | ||
![]() |
1f16f35ef2 | ||
![]() |
cbcd67e884 | ||
![]() |
5c78854b61 | ||
![]() |
f3fbf4a29f | ||
![]() |
460e3a2726 | ||
![]() |
891f15dddf | ||
![]() |
0a5a6ef0a4 | ||
![]() |
d59c6c069b | ||
![]() |
e2d87483c9 | ||
![]() |
cb1397e472 | ||
![]() |
4b84396f83 | ||
![]() |
c6ff3283d6 | ||
![]() |
a45e2bd379 | ||
![]() |
84ba14ef7c | ||
![]() |
fcb3e6307f | ||
![]() |
65c0fbf448 | ||
![]() |
b6f5736784 | ||
![]() |
91b5d5462f | ||
![]() |
878effcfb1 | ||
![]() |
2ff80eb759 | ||
![]() |
bb21cf7077 | ||
![]() |
9ad64fbe9e | ||
![]() |
699d582a75 | ||
![]() |
e2085f7210 | ||
![]() |
9ac854268c | ||
![]() |
aeb387a92d | ||
![]() |
9f3308e422 | ||
![]() |
9b07e72065 | ||
![]() |
36db7dd37d | ||
![]() |
ef5e3361cd | ||
![]() |
e81a1ef3a3 | ||
![]() |
87a0471225 | ||
![]() |
e74d20a60a | ||
![]() |
e351ccdfa5 | ||
![]() |
6c584c5d9d | ||
![]() |
fc3861b4f0 | ||
![]() |
5add2e167d | ||
![]() |
e834e5e316 | ||
![]() |
cfac9d0310 | ||
![]() |
548779b08d | ||
![]() |
3190b35d18 | ||
![]() |
b16dedd9de | ||
![]() |
f5c40e4723 | ||
![]() |
5a3e29c5d6 | ||
![]() |
8190cb971d | ||
![]() |
200f23fc18 | ||
![]() |
e62d875ec6 | ||
![]() |
2edcb7e7fd | ||
![]() |
3521143cd3 | ||
![]() |
8329838383 | ||
![]() |
78a0c4de62 | ||
![]() |
5ce43e7e39 | ||
![]() |
4a56f44b48 | ||
![]() |
be770a0fc4 | ||
![]() |
e5984ccffe | ||
![]() |
66f26862ac | ||
![]() |
eac45f58f4 | ||
![]() |
567abd352e | ||
![]() |
2e355e2d04 | ||
![]() |
feaad0842c | ||
![]() |
8e8d77bc6e | ||
![]() |
4f490dc153 | ||
![]() |
145a6cfb7e | ||
![]() |
55f5881e60 | ||
![]() |
64c560396d | ||
![]() |
4c472f1af2 | ||
![]() |
f652d3763e | ||
![]() |
9b4dcf8a2d | ||
![]() |
941e86919d | ||
![]() |
6f142efb42 | ||
![]() |
174e9c4738 | ||
![]() |
537f1fa805 | ||
![]() |
fd2918b6cf | ||
![]() |
87babe55de | ||
![]() |
791d9e66bf | ||
![]() |
245ac6df4f | ||
![]() |
0cc892e6e6 | ||
![]() |
28a7309328 | ||
![]() |
fb184d75fd | ||
![]() |
9910ed2928 | ||
![]() |
2fd6e0a078 | ||
![]() |
c3891eb870 | ||
![]() |
59fe7fbe70 | ||
![]() |
5129627b26 | ||
![]() |
3e91dbed43 | ||
![]() |
73d3ab20a8 | ||
![]() |
09cf54e003 | ||
![]() |
151a4eda64 | ||
![]() |
8ae7fb95b3 | ||
![]() |
5f08aaf361 | ||
![]() |
8c6537e2a6 | ||
![]() |
eed08ca1d7 | ||
![]() |
945d648035 | ||
![]() |
556cdad01c | ||
![]() |
6c453a852e | ||
![]() |
8b3be6839e | ||
![]() |
1233d181f8 | ||
![]() |
e9f22048c5 | ||
![]() |
f086827963 | ||
![]() |
15be98fd7b | ||
![]() |
0f1151af71 | ||
![]() |
dc3b736901 | ||
![]() |
2047ae83e1 | ||
![]() |
ac2897bd6f | ||
![]() |
c95181f909 | ||
![]() |
cbb27671c7 | ||
![]() |
d4a9f76091 | ||
![]() |
a47359325a | ||
![]() |
395ad534b1 | ||
![]() |
ddb71b79a9 | ||
![]() |
e502dc50f3 | ||
![]() |
1385babdfb | ||
![]() |
77a2622111 | ||
![]() |
783f271d4c | ||
![]() |
d871a7867b | ||
![]() |
21ca588a34 | ||
![]() |
15d2c5298a | ||
![]() |
8a3da023d1 | ||
![]() |
1e5b9bcea1 | ||
![]() |
22c94961c5 | ||
![]() |
56176ba360 | ||
![]() |
9af6bbaa1f | ||
![]() |
05dd3a8308 | ||
![]() |
80b4965119 | ||
![]() |
3b301b79e1 | ||
![]() |
a11ed9a31f | ||
![]() |
57aff6082e | ||
![]() |
dc9e4db126 | ||
![]() |
95419372d2 | ||
![]() |
f520e641fb | ||
![]() |
37fede6ba8 | ||
![]() |
3f3035d581 | ||
![]() |
f0bb24b9cb | ||
![]() |
78a56f9157 | ||
![]() |
53ff8f2c0d | ||
![]() |
a58eeee607 | ||
![]() |
e027d667b0 | ||
![]() |
f7f5845e2b | ||
![]() |
f2b3ec04ba | ||
![]() |
cff5e241ce | ||
![]() |
68fab5c95a | ||
![]() |
0d37154516 | ||
![]() |
2b4933aaba | ||
![]() |
62353ff310 | ||
![]() |
972beef9c4 | ||
![]() |
57a8b42154 | ||
![]() |
a925d0ae89 | ||
![]() |
89c932d190 | ||
![]() |
db9abd4fb1 | ||
![]() |
1d5e225fd4 | ||
![]() |
d3290c1cfe | ||
![]() |
1900b2507c | ||
![]() |
334760d37a | ||
![]() |
6119c6ee7e | ||
![]() |
0a1d4a8602 | ||
![]() |
947f3cf1d7 | ||
![]() |
6a9bec3153 | ||
![]() |
c6696bd2f2 | ||
![]() |
f9ca490190 | ||
![]() |
60d4cc4df6 | ||
![]() |
837443151a | ||
![]() |
d0ca999a3b | ||
![]() |
4c888997e2 | ||
![]() |
249db0c535 | ||
![]() |
8f96df23f2 | ||
![]() |
80d8ea8aeb | ||
![]() |
c4195a31cb | ||
![]() |
4817e88f7a | ||
![]() |
c854fc6eb4 | ||
![]() |
f51d7dfff1 | ||
![]() |
d3f02319c4 | ||
![]() |
92b12cb446 | ||
![]() |
52ca80ecb7 | ||
![]() |
e71bbc97a5 | ||
![]() |
9977f694c8 | ||
![]() |
1823d674e0 | ||
![]() |
c5199d8568 | ||
![]() |
9cb4461560 | ||
![]() |
2c0f0bd894 | ||
![]() |
2eb47a3b11 | ||
![]() |
1635cdb3b2 | ||
![]() |
d0e3852b59 | ||
![]() |
d6df3009d8 | ||
![]() |
c5efb90123 | ||
![]() |
2ede0b1214 | ||
![]() |
cc6e3ad5e1 | ||
![]() |
6c32a6ad64 | ||
![]() |
a8e201a13b | ||
![]() |
0e0fdb000f | ||
![]() |
eef8468d8f | ||
![]() |
525693fcdc | ||
![]() |
1cf3683ff2 | ||
![]() |
c5b2708697 | ||
![]() |
9a51c30d55 |
42
.circleci/config.yml
Normal file
|
@ -0,0 +1,42 @@
|
|||
version: 2.1
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
docker:
|
||||
- image: cimg/node:22.9
|
||||
resource_class: medium
|
||||
steps:
|
||||
- add_ssh_keys:
|
||||
fingerprints:
|
||||
- "86:3b:c9:a6:d1:b9:a8:dc:0e:00:db:99:8d:19:c4:3e"
|
||||
- run:
|
||||
name: Add known hosts
|
||||
command: |
|
||||
mkdir -p ~/.ssh
|
||||
echo $GH_HOST >> ~/.ssh/known_hosts
|
||||
echo $RPM_HOST >> ~/.ssh/known_hosts
|
||||
- run:
|
||||
name: Install NPM
|
||||
command: |
|
||||
sudo apt-get update && sudo apt-get install -y rsync npm
|
||||
- run:
|
||||
name: Clone WebVM
|
||||
command: |
|
||||
git clone --branch $CIRCLE_BRANCH --single-branch git@github.com:leaningtech/webvm.git
|
||||
- run:
|
||||
name: Build WebVM
|
||||
command: |
|
||||
cd webvm/
|
||||
npm install
|
||||
npm run build
|
||||
- run:
|
||||
name: Deploy webvm
|
||||
command: |
|
||||
rsync -avz -e "ssh -p ${SSH_PORT}" webvm/build/ leaningtech@${SSH_HOST}:/srv/web/webvm/
|
||||
|
||||
workflows:
|
||||
deploy:
|
||||
when:
|
||||
equal: [ << pipeline.trigger_source >>, "api" ]
|
||||
jobs:
|
||||
- deploy
|
245
.github/workflows/deploy.yml
vendored
Normal file
|
@ -0,0 +1,245 @@
|
|||
name: Deploy
|
||||
|
||||
# Define when the workflow should run
|
||||
on:
|
||||
# Allow manual triggering of the workflow from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Allow inputs to be passed when manually triggering the workflow from the Actions tab
|
||||
inputs:
|
||||
DOCKERFILE_PATH:
|
||||
type: string
|
||||
description: 'Path to the Dockerfile'
|
||||
required: true
|
||||
default: 'dockerfiles/debian_mini'
|
||||
|
||||
IMAGE_SIZE:
|
||||
type: string
|
||||
description: 'Image size, 950M max'
|
||||
required: true
|
||||
default: '600M'
|
||||
|
||||
DEPLOY_TO_GITHUB_PAGES:
|
||||
type: boolean
|
||||
description: 'Deploy to Github pages'
|
||||
required: true
|
||||
default: true
|
||||
|
||||
GITHUB_RELEASE:
|
||||
type: boolean
|
||||
description: 'Upload GitHub release'
|
||||
required: true
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
|
||||
guard_clause:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }} # As required by the GitHub-CLI
|
||||
|
||||
permissions:
|
||||
actions: 'write' # Required in order to terminate the workflow run.
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
# Guard clause that cancels the workflow in case of an invalid DOCKERFILE_PATH and/or incorrectly configured Github Pages.
|
||||
# The main reason for choosing this workaround for aborting the workflow is the fact that it does not display the workflow as successful, which can set false expectations.
|
||||
- name: DOCKERFILE_PATH.
|
||||
shell: bash
|
||||
run: |
|
||||
# We check whether the Dockerfile_path is valid.
|
||||
if [ ! -f ${{ github.event.inputs.DOCKERFILE_PATH }} ]; then
|
||||
echo "::error title=Invalid Dockerfile path::No file found at ${{ github.event.inputs.DOCKERFILE_PATH }}"
|
||||
echo "terminate=true" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Github Pages config guard clause
|
||||
if: ${{ github.event.inputs.DEPLOY_TO_GITHUB_PAGES == 'true' }}
|
||||
run: |
|
||||
# We use the Github Rest api to get information regarding pages for the Github Repository and store it into a temporary file named "pages_response".
|
||||
set +e
|
||||
gh api \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
/repos/${{ github.repository_owner }}/$(basename ${{ github.repository }})/pages > pages_response
|
||||
|
||||
# We make sure Github Pages has been enabled for this repository.
|
||||
if [ "$?" -ne 0 ]; then
|
||||
echo "::error title=Potential pages configuration error.::Please make sure you have enabled Github pages for the ${{ github.repository }} repository. If already enabled then Github pages might be down"
|
||||
echo "terminate=true" >> $GITHUB_ENV
|
||||
fi
|
||||
set -e
|
||||
|
||||
# We make sure the Github pages build & deployment source is set to "workflow" (Github Actions). Instead of a "legacy" (branch).
|
||||
if [[ "$(jq --compact-output --raw-output .build_type pages_response)" != "workflow" ]]; then
|
||||
echo "Undefined behaviour, Make sure the Github Pages source is correctly configured in the Github Pages settings."
|
||||
echo "::error title=Pages configuration error.::Please make sure you have correctly picked \"Github Actions\" as the build and deployment source for the Github Pages."
|
||||
echo "terminate=true" >> $GITHUB_ENV
|
||||
fi
|
||||
rm pages_response
|
||||
|
||||
- name: Terminate run if error occurred.
|
||||
run: |
|
||||
if [[ $terminate == "true" ]]; then
|
||||
gh run cancel ${{ github.run_id }}
|
||||
gh run watch ${{ github.run_id }}
|
||||
fi
|
||||
|
||||
build:
|
||||
needs: guard_clause # Dependency
|
||||
runs-on: ubuntu-latest # Image to run the worker on.
|
||||
|
||||
env:
|
||||
TAG: "ext2-webvm-base-image" # Tag of docker image.
|
||||
IMAGE_SIZE: '${{ github.event.inputs.IMAGE_SIZE }}'
|
||||
DEPLOY_DIR: /webvm_deploy/ # Path to directory where we host the final image from.
|
||||
|
||||
permissions: # Permissions to grant the GITHUB_TOKEN.
|
||||
contents: write # Required permission to make a github release.
|
||||
|
||||
steps:
|
||||
# Checks-out our repository under $GITHUB_WORKSPACE, so our job can access it
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
# Setting the IMAGE_NAME variable in GITHUB_ENV to <Dockerfile name>_<date>_<run_id>.ext2.
|
||||
- name: Generate the image_name.
|
||||
id: image_name_gen
|
||||
run: |
|
||||
echo "IMAGE_NAME=$(basename ${{ github.event.inputs.DOCKERFILE_PATH }})_$(date +%Y%m%d)_${{ github.run_id }}.ext2" >> $GITHUB_ENV
|
||||
|
||||
# Create directory to host the image from.
|
||||
- run: sudo mkdir -p $DEPLOY_DIR
|
||||
|
||||
# Build the i386 Dockerfile image.
|
||||
- run: docker build . --tag $TAG --file ${{ github.event.inputs.DOCKERFILE_PATH }} --platform=i386
|
||||
|
||||
# Run the docker image so that we can export the container.
|
||||
# Run the Docker container with the Google Public DNS nameservers: 8.8.8.8, 8.8.4.4
|
||||
- run: |
|
||||
docker run --dns 8.8.8.8 --dns 8.8.4.4 -d $TAG
|
||||
echo "CONTAINER_ID=$(sudo docker ps -aq)" >> $GITHUB_ENV
|
||||
|
||||
# We extract the CMD, we first need to figure whether the Dockerfile uses CMD or an Entrypoint.
|
||||
- name: Extracting CMD / Entrypoint and args
|
||||
shell: bash
|
||||
run: |
|
||||
cmd=$(sudo docker inspect --format='{{json .Config.Cmd}}' $CONTAINER_ID)
|
||||
entrypoint=$(sudo docker inspect --format='{{json .Config.Entrypoint}}' $CONTAINER_ID)
|
||||
if [[ $entrypoint != "null" && $cmd != "null" ]]; then
|
||||
echo "CMD=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Entrypoint' )" >> $GITHUB_ENV
|
||||
echo "ARGS=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Cmd' )" >> $GITHUB_ENV
|
||||
elif [[ $cmd != "null" ]]; then
|
||||
echo "CMD=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Cmd[:1]' )" >> $GITHUB_ENV
|
||||
echo "ARGS=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Cmd[1:]' )" >> $GITHUB_ENV
|
||||
else
|
||||
echo "CMD=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Entrypoint[:1]' )" >> $GITHUB_ENV
|
||||
echo "ARGS=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Entrypoint[1:]' )" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
# We extract the ENV, CMD/Entrypoint and cwd from the Docker container with docker inspect.
|
||||
- name: Extracting env, args and cwd.
|
||||
shell: bash
|
||||
run: |
|
||||
echo "ENV=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Env' )" >> $GITHUB_ENV
|
||||
echo "CWD=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.WorkingDir' )" >> $GITHUB_ENV
|
||||
|
||||
# We create and mount the base ext2 image to extract the Docker container's filesystem its contents into.
|
||||
- name: Create ext2 image.
|
||||
run: |
|
||||
# Preallocate space for the ext2 image
|
||||
sudo fallocate -l $IMAGE_SIZE ${IMAGE_NAME}
|
||||
# Format to ext2 linux kernel revision 0
|
||||
sudo mkfs.ext2 -r 0 ${IMAGE_NAME}
|
||||
# Mount the ext2 image to modify it
|
||||
sudo mount -o loop -t ext2 ${IMAGE_NAME} /mnt/
|
||||
|
||||
# We opt for 'docker cp --archive' over 'docker save' since our focus is solely on the end product rather than individual layers and metadata.
|
||||
# However, it's important to note that despite being specified in the documentation, the '--archive' flag does not currently preserve uid/gid information when copying files from the container to the host machine.
|
||||
# Another compelling reason to use 'docker cp' is that it preserves resolv.conf.
|
||||
- name: Export and unpack container filesystem contents into mounted ext2 image.
|
||||
run: |
|
||||
sudo docker cp -a ${CONTAINER_ID}:/ /mnt/
|
||||
sudo umount /mnt/
|
||||
# Result is an ext2 image for webvm.
|
||||
|
||||
# The .txt suffix enabled HTTP compression for free
|
||||
- name: Generate image split chunks and .meta file
|
||||
run: |
|
||||
sudo split ${{ env.IMAGE_NAME }} ${{ env.DEPLOY_DIR }}/${{ env.IMAGE_NAME }}.c -a 6 -b 128k -x --additional-suffix=.txt
|
||||
sudo bash -c "stat -c%s ${{ env.IMAGE_NAME }} > ${{ env.DEPLOY_DIR }}/${{ env.IMAGE_NAME }}.meta"
|
||||
|
||||
# This step updates the default config_github_terminal.js file by performing the following actions:
|
||||
# 1. Replaces all occurrences of IMAGE_URL with the URL to the image.
|
||||
# 2. Replace CMD with the Dockerfile entry command.
|
||||
# 3. Replace args with the Dockerfile CMD / Entrypoint args.
|
||||
# 4. Replace ENV with the container's environment values.
|
||||
# 5. Replace CWD with the container's current working directory.
|
||||
- name: Adjust config_github_terminal.js
|
||||
run: |
|
||||
sed -i 's#IMAGE_URL#"${{ env.IMAGE_NAME }}"#g' config_github_terminal.js
|
||||
sed -i 's#CMD#${{ env.CMD }}#g' config_github_terminal.js
|
||||
sed -i 's#ARGS#${{ env.ARGS }}#g' config_github_terminal.js
|
||||
sed -i 's#ENV#${{ env.ENV }}#g' config_github_terminal.js
|
||||
sed -i 's#CWD#${{ env.CWD }}#g' config_github_terminal.js
|
||||
|
||||
- name: Build NPM package
|
||||
run: |
|
||||
npm install
|
||||
WEBVM_MODE=github npm run build
|
||||
|
||||
# Move required files for gh-pages deployment to the deployment directory $DEPLOY_DIR.
|
||||
- name: Copy build
|
||||
run: |
|
||||
rm build/alpine.html
|
||||
sudo mv build/* $DEPLOY_DIR/
|
||||
|
||||
# We generate index.list files for our httpfs to function properly.
|
||||
- name: make index.list
|
||||
shell: bash
|
||||
run: |
|
||||
find $DEPLOY_DIR -type d | while read -r dir;
|
||||
do
|
||||
index_list="$dir/index.list";
|
||||
sudo rm -f "$index_list";
|
||||
sudo ls "$dir" | sudo tee "$index_list" > /dev/null;
|
||||
sudo chmod +rw "$index_list";
|
||||
sudo echo "created $index_list";
|
||||
done
|
||||
|
||||
# Create a gh-pages artifact in order to deploy to gh-pages.
|
||||
- name: Upload GitHub Pages artifact
|
||||
uses: actions/upload-pages-artifact@v2
|
||||
with:
|
||||
# Path of the directory containing the static assets for our gh pages deployment.
|
||||
path: ${{ env.DEPLOY_DIR }} # optional, default is _site/
|
||||
|
||||
- name: github release # To upload our final ext2 image as a github release.
|
||||
if: ${{ github.event.inputs.GITHUB_RELEASE == 'true' }}
|
||||
uses: softprops/action-gh-release@v0.1.15
|
||||
with:
|
||||
target_commitish: ${{ github.sha }} # Last commit on the GITHUB_REF branch or tag
|
||||
tag_name: ext2_image
|
||||
fail_on_unmatched_files: 'true' # Fail in case of no matches with the file(s) glob(s).
|
||||
files: | # Assets to upload as release.
|
||||
${{ env.IMAGE_NAME }}
|
||||
|
||||
deploy_to_github_pages: # Job that deploys the github-pages artifact to github-pages.
|
||||
if: ${{ github.event.inputs.DEPLOY_TO_GITHUB_PAGES == 'true' }}
|
||||
needs: build
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
|
||||
permissions:
|
||||
pages: write # to deploy to Pages
|
||||
id-token: write # to verify the deployment originates from an appropriate source
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Deployment to github pages
|
||||
- name: Deploy GitHub Pages site
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v3
|
1
.npmrc
Normal file
|
@ -0,0 +1 @@
|
|||
engine-strict=true
|
202
LICENSE.txt
Normal file
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
126
README.md
|
@ -1,42 +1,118 @@
|
|||
# WebVM
|
||||
|
||||
This repository hosts the source code of the [https://webvm.io](https://webvm.io) live demo page.
|
||||
[](https://discord.gg/yWRr2YnD9c)
|
||||
[](https://github.com/leaningtech/webvm/issues)
|
||||
|
||||
WebVM is a server-less virtual environment running fully client-side in HTML5/WebAssembly. It's designed to be Linux ABI-compatible. In this demo, it runs an unmodified Debian distribution including many native development toolchains.
|
||||
This repository hosts the source code for [https://webvm.io](https://webvm.io), a Linux virtual machine that runs in your browser.
|
||||
|
||||
<img src="/assets/welcome_to_WebVM_2024.png" width="70%">
|
||||
|
||||
WebVM is a server-less virtual environment running fully client-side in HTML5/WebAssembly. It's designed to be Linux ABI-compatible. It runs an unmodified Debian distribution including many native development toolchains.
|
||||
|
||||
WebVM is powered by the CheerpX virtualization engine, and enables safe, sandboxed client-side execution of x86 binaries on any browser. CheerpX includes an x86-to-WebAssembly JIT compiler, a virtual block-based file system, and a Linux syscall emulator.
|
||||
|
||||
For more information: https://medium.com/leaningtech/webvm-client-side-x86-virtual-machines-in-the-browser-40a60170b361
|
||||
# Enable networking
|
||||
|
||||
Modern browsers do not provide APIs to directly use TCP or UDP. WebVM provides networking support by integrating with Tailscale, a VPN network that supports WebSockets as a transport layer.
|
||||
|
||||
- Open the "Networking" panel from the side-bar
|
||||
- Click "Connect to Tailscale" from the panel
|
||||
- Log in to Tailscale (create an account if you don't have one)
|
||||
- Click "Connect" when prompted by Tailscale
|
||||
- If you are unfamiliar with Tailscale or would like additional information see [WebVM and Tailscale](/docs/Tailscale.md).
|
||||
|
||||
# Fork, deploy, customize
|
||||
|
||||
<img src="/assets/fork_deploy_instructions.gif" alt="deploy_instructions_gif" width="90%">
|
||||
|
||||
- Fork the repository.
|
||||
- Enable Github pages in settings.
|
||||
- Click on `Settings`.
|
||||
- Go to the `Pages` section.
|
||||
- Select `Github Actions` as the source.
|
||||
- If you are using a custom domain, ensure `Enforce HTTPS` is enabled.
|
||||
- Run the workflow.
|
||||
- Click on `Actions`.
|
||||
- Accept the prompt. This is required only once to enable Actions for your fork.
|
||||
- Click on the workflow named `Deploy`.
|
||||
- Click `Run workflow` and then once more `Run workflow` in the menu.
|
||||
- After a few seconds a new `Deploy` workflow will start, click on it to see details.
|
||||
- After the workflow completes, which takes a few minutes, it will show the URL below the `deploy_to_github_pages` job.
|
||||
|
||||
<img src="/assets/result.png" width="70%" >
|
||||
|
||||
You can now customize `dockerfiles/debian_mini` to suit your needs, or make a new Dockerfile from scratch. Use the `Path to Dockerfile` workflow parameter to select it.
|
||||
|
||||
# Local deployment
|
||||
|
||||
From a local `git clone`
|
||||
|
||||
- Download the `debian_mini` Ext2 image from [https://github.com/leaningtech/webvm/releases/](https://github.com/leaningtech/webvm/releases/)
|
||||
- You can also build your own by selecting the "Upload GitHub release" workflow option
|
||||
- Place the image in the repository root folder
|
||||
- Edit `config_github_terminal.js`
|
||||
- Uncomment the default values for `CMD`, `ARGS`, `ENV` and `CWD`
|
||||
- Replace `IMAGE_URL` with the URL (absolute or relative) for the Ext2 image. For example `"/debian_mini_20230519_5022088024.ext2"`
|
||||
- Build WebVM using `npm`, output will be placed in the `build` directory
|
||||
- `npm install`
|
||||
- `npm run build`
|
||||
- Start NGINX, it automatically points to the `build` directory just created
|
||||
- `nginx -p . -c nginx.conf`
|
||||
- Visit `http://127.0.0.1:8081` and enjoy your local WebVM
|
||||
|
||||
# Example customization: Python3 REPL
|
||||
|
||||
The `Deploy` workflow takes into account the `CMD` specified in the Dockerfile. To build a REPL you can simply apply this patch and deploy.
|
||||
|
||||
```diff
|
||||
diff --git a/dockerfiles/debian_mini b/dockerfiles/debian_mini
|
||||
index 2878332..1f3103a 100644
|
||||
--- a/dockerfiles/debian_mini
|
||||
+++ b/dockerfiles/debian_mini
|
||||
@@ -15,4 +15,4 @@ WORKDIR /home/user/
|
||||
# We set env, as this gets extracted by Webvm. This is optional.
|
||||
ENV HOME="/home/user" TERM="xterm" USER="user" SHELL="/bin/bash" EDITOR="vim" LANG="en_US.UTF-8" LC_ALL="C"
|
||||
RUN echo 'root:password' | chpasswd
|
||||
-CMD [ "/bin/bash" ]
|
||||
+CMD [ "/usr/bin/python3" ]
|
||||
```
|
||||
|
||||
# Bugs and Issues
|
||||
|
||||
Please use [Issues](github.com/webvm/issues) to report any bug.
|
||||
Please use [Issues](https://github.com/leaningtech/webvm/issues) to report any bug.
|
||||
Or come to say hello / share your feedback on [Discord](https://discord.gg/yTNZgySKGa).
|
||||
|
||||
## Browsers support
|
||||
# More links
|
||||
|
||||
|<br>Chrome|<br>Edge|<br>Safari|<br>Firefox|
|
||||
|:---:|:---:|:---:|:---:|
|
||||
|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
||||
- [WebVM: server-less x86 virtual machines in the browser](https://leaningtech.com/webvm-server-less-x86-virtual-machines-in-the-browser/)
|
||||
- [WebVM: Linux Virtualization in WebAssembly with Full Networking via Tailscale](https://leaningtech.com/webvm-virtual-machine-with-networking-via-tailscale/)
|
||||
- [Mini.WebVM: Your own Linux box from Dockerfile, virtualized in the browser via WebAssembly](https://leaningtech.com/mini-webvm-your-linux-box-from-dockerfile-via-wasm/)
|
||||
- Reference GitHub Pages deployment: [Mini.WebVM](https://mini.webvm.io)
|
||||
- [Crafting the Impossible: X86 Virtualization in the Browser with WebAssembly](https://www.youtube.com/watch?v=VqrbVycTXmw) Talk at JsNation 2022
|
||||
|
||||
WebVM and CheerpX are compatible with any browser, both on Desktop (Chrome/Chromium, Edge, Firefox, Safari), and Mobile (Chrome, Safari), provided support for [SAB](https://medium.com/r?url=https%3A%2F%2Fcaniuse.com%2F%3Fsearch%3DSharedArrayBuffer) is present, and the device has sufficient memory.
|
||||
# Thanks to...
|
||||
This project depends on:
|
||||
- [CheerpX](https://cheerpx.io/), made by [Leaning Technologies](https://leaningtech.com/) for x86 virtualization and Linux emulation
|
||||
- xterm.js, [https://xtermjs.org/](https://xtermjs.org/), for providing the Web-based terminal emulator
|
||||
- [Tailscale](https://tailscale.com/), for the networking component
|
||||
- [lwIP](https://savannah.nongnu.org/projects/lwip/), for the TCP/IP stack, compiled for the Web via [Cheerp](https://github.com/leaningtech/cheerp-meta/)
|
||||
|
||||
# Other
|
||||
# Versioning
|
||||
|
||||
This project depends on xterm.js (https://xtermjs.org/) and on its add-on xterm-addon-fit
|
||||
WebVM depends on the CheerpX x86-to-WebAssembly virtualization technology, which is included in the project via [NPM](https://www.npmjs.com/package/@leaningtech/cheerpx).
|
||||
|
||||
To update the xterm-related files do:
|
||||
```
|
||||
mkdir build
|
||||
cd build
|
||||
npm install --save xterm
|
||||
npm install --save xterm-addon-fit
|
||||
cd ../xterm
|
||||
cp ../build/node_modules/xterm/lib/xterm.js .
|
||||
cp ../build/node_modules/xterm/css/xterm.css .
|
||||
cp ../build/node_modules/xterm-addon-fit/lib/xterm-addon-fit.js .
|
||||
cd ..
|
||||
rm -r build
|
||||
```
|
||||
The NPM package is updated on every release.
|
||||
|
||||
Every build is immutable, if a specific version works well for you today, it will keep working forever.
|
||||
|
||||
# License
|
||||
Copyright (c) Leaning Technologies Limited. All rights reserved.
|
||||
|
||||
WebVM is released under the Apache License, Version 2.0.
|
||||
|
||||
You are welcome to use, modify, and redistribute the contents of this repository.
|
||||
|
||||
The public CheerpX deployment is provided **as-is** and is **free to use** for technological exploration, testing and use by individuals. Any other use by organizations, including non-profit, academia and the public sector, requires a license. Downloading a CheerpX build for the purpose of hosting it elsewhere is not permitted without a commercial license.
|
||||
|
||||
Read more about [CheerpX licensing](https://cheerpx.io/docs/licensing)
|
||||
|
||||
If you want to build a product on top of CheerpX/WebVM, please get in touch: sales@leaningtech.com
|
||||
|
|
0
a
BIN
assets/alpine_bg.png
Normal file
After Width: | Height: | Size: 82 KiB |
1
assets/cheerpx.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 79.37 89.83"><defs><style>.cls-1{fill:#4b647f;}.cls-2{fill:#6386a5;}.cls-3{fill:#e2e2e2;}.cls-4{fill:#fff;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><g id="code_html5"><polygon class="cls-1" points="79.37 0 39.69 0 39.69 0 0 0 7.44 80.69 39.69 89.83 39.69 89.83 39.69 89.83 39.69 89.83 39.69 89.83 71.92 80.69 79.37 0"/><polygon class="cls-2" points="39.69 6.57 39.69 82.99 39.69 82.99 65.8 75.59 72.16 6.57 39.69 6.57"/><path class="cls-3" d="M39.52,50.42a7.7,7.7,0,1,1,0-15.4h.17V19.66c-1.16,0-2.31,0-3.46,0a1,1,0,0,0-1.15.95c-.29,1.68-.55,3.36-.79,5.05a.61.61,0,0,1-.45.57c-.72.27-1.42.6-2.13.89a.48.48,0,0,1-.39,0q-1.84-1.41-3.66-2.83c-.88-.7-1.28-.71-2.2.05a34.62,34.62,0,0,0-4.54,4.6c-.57.7-.55,1.05,0,1.76l2.92,3.75a.42.42,0,0,1,0,.52c-.33.73-.65,1.45-.94,2.2a.45.45,0,0,1-.4.33c-1.57.23-3.13.49-4.7.69a1.59,1.59,0,0,0-1.4.89v7.22a1.45,1.45,0,0,0,1.35.9c1.56.2,3.11.44,4.66.67a.53.53,0,0,1,.49.4c.28.77.6,1.53.94,2.27a.56.56,0,0,1-.05.66c-1,1.23-1.93,2.48-2.88,3.73A1.26,1.26,0,0,0,21,56.57,45.34,45.34,0,0,0,25.7,61.3a1.08,1.08,0,0,0,1.67.1c1.34-1,2.67-2,4-3a.48.48,0,0,1,.37,0c.76.3,1.5.63,2.25.92a.39.39,0,0,1,.29.36c.24,1.61.51,3.22.73,4.84a1.42,1.42,0,0,0,.88,1.32h3.79V50.41Z"/><path class="cls-4" d="M61.39,38.31l-4.88-.73a.5.5,0,0,1-.43-.37q-.45-1.17-1-2.31a.48.48,0,0,1,.05-.59c1-1.24,1.94-2.5,2.9-3.76A1.23,1.23,0,0,0,58,28.9a49.11,49.11,0,0,0-4.68-4.78A1.1,1.1,0,0,0,51.67,24l-4,3a.54.54,0,0,1-.41.06c-.75-.29-1.47-.62-2.22-.91a.4.4,0,0,1-.31-.38c-.25-1.7-.52-3.4-.78-5.09a1.08,1.08,0,0,0-1.19-1.05c-1,0-2,0-3.06,0V35a7.69,7.69,0,0,1,0,15.38V65.79h3.5A1.45,1.45,0,0,0,44,64.54c.24-1.62.5-3.23.73-4.84a.5.5,0,0,1,.35-.45c.74-.29,1.46-.62,2.2-.92a.51.51,0,0,1,.42.05c1.2.92,2.4,1.86,3.6,2.8.9.72,1.32.74,2.23,0a35.07,35.07,0,0,0,4.57-4.63c.56-.69.54-1.05,0-1.76l-2.9-3.72a.49.49,0,0,1-.06-.59c.34-.72.63-1.46,1-2.18a.5.5,0,0,1,.32-.27c1.68-.27,3.37-.53,5.06-.78A1.13,1.13,0,0,0,62.57,46c0-2.11,0-4.23,0-6.34A1.18,1.18,0,0,0,61.39,38.31Z"/></g></g></g></svg>
|
After Width: | Height: | Size: 2 KiB |
1
assets/discord-mark-blue.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"><defs><style>.cls-1{fill:#5865f2;}</style></defs><g id="图层_2" data-name="图层 2"><g id="Discord_Logos" data-name="Discord Logos"><g id="Discord_Logo_-_Large_-_White" data-name="Discord Logo - Large - White"><path class="cls-1" d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"/></g></g></g></svg>
|
After Width: | Height: | Size: 988 B |
BIN
assets/fork_deploy_instructions.gif
Normal file
After Width: | Height: | Size: 1.5 MiB |
1
assets/github-mark-white.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>
|
After Width: | Height: | Size: 960 B |
BIN
assets/leaningtech.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/result.png
Normal file
After Width: | Height: | Size: 8.3 KiB |
BIN
assets/social_2024.png
Normal file
After Width: | Height: | Size: 185 KiB |
11
assets/tailscale.svg
Normal file
|
@ -0,0 +1,11 @@
|
|||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<ellipse cx="2.89214" cy="11.7148" rx="2.89214" ry="2.88514" fill="white"></ellipse>
|
||||
<ellipse cx="11.5685" cy="11.7148" rx="2.89214" ry="2.88514" fill="white"></ellipse>
|
||||
<ellipse opacity="0.2" cx="2.89214" cy="20.3703" rx="2.89214" ry="2.88514" fill="white"></ellipse>
|
||||
<ellipse opacity="0.2" cx="20.245" cy="20.3703" rx="2.89214" ry="2.88514" fill="white"></ellipse>
|
||||
<ellipse cx="11.5685" cy="20.3703" rx="2.89214" ry="2.88514" fill="white"></ellipse>
|
||||
<ellipse cx="20.245" cy="11.7148" rx="2.89214" ry="2.88514" fill="white"></ellipse>
|
||||
<ellipse opacity="0.2" cx="2.89214" cy="3.0594" rx="2.89214" ry="2.88514" fill="white"></ellipse>
|
||||
<ellipse opacity="0.2" cx="11.5685" cy="3.0594" rx="2.89214" ry="2.88514" fill="white"></ellipse>
|
||||
<ellipse opacity="0.2" cx="20.245" cy="3.0594" rx="2.89214" ry="2.88514" fill="white"></ellipse>
|
||||
</svg>
|
After Width: | Height: | Size: 1,007 B |
Before Width: | Height: | Size: 122 KiB |
BIN
assets/webvm_hero.png
Normal file
After Width: | Height: | Size: 514 KiB |
BIN
assets/welcome_to_WebVM_2024.png
Normal file
After Width: | Height: | Size: 248 KiB |
23
config_github_terminal.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
// The root filesystem location
|
||||
export const diskImageUrl = IMAGE_URL;
|
||||
// The root filesystem backend type
|
||||
export const diskImageType = "github";
|
||||
// Print an introduction message about the technology
|
||||
export const printIntro = true;
|
||||
// Is a graphical display needed
|
||||
export const needsDisplay = false;
|
||||
// Executable full path (Required)
|
||||
export const cmd = CMD; // Default: "/bin/bash";
|
||||
// Arguments, as an array (Required)
|
||||
export const args = ARGS; // Default: ["--login"];
|
||||
// Optional extra parameters
|
||||
export const opts = {
|
||||
// Environment variables
|
||||
env: ENV, // Default: ["HOME=/home/user", "TERM=xterm", "USER=user", "SHELL=/bin/bash", "EDITOR=vim", "LANG=en_US.UTF-8", "LC_ALL=C"],
|
||||
// Current working directory
|
||||
cwd: CWD, // Default: "/home/user",
|
||||
// User id
|
||||
uid: 1000,
|
||||
// Group id
|
||||
gid: 1000
|
||||
};
|
19
config_public_alpine.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
// The root filesystem location
|
||||
export const diskImageUrl = "wss://disks.webvm.io/alpine_20241109.ext2";
|
||||
// The root filesystem backend type
|
||||
export const diskImageType = "cloud";
|
||||
// Print an introduction message about the technology
|
||||
export const printIntro = false;
|
||||
// Is a graphical display needed
|
||||
export const needsDisplay = true;
|
||||
// Executable full path (Required)
|
||||
export const cmd = "/sbin/init";
|
||||
// Arguments, as an array (Required)
|
||||
export const args = [];
|
||||
// Optional extra parameters
|
||||
export const opts = {
|
||||
// User id
|
||||
uid: 0,
|
||||
// Group id
|
||||
gid: 0
|
||||
};
|
23
config_public_terminal.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
// The root filesystem location
|
||||
export const diskImageUrl = "wss://disks.webvm.io/debian_large_20230522_5044875331.ext2";
|
||||
// The root filesystem backend type
|
||||
export const diskImageType = "cloud";
|
||||
// Print an introduction message about the technology
|
||||
export const printIntro = true;
|
||||
// Is a graphical display needed
|
||||
export const needsDisplay = false;
|
||||
// Executable full path (Required)
|
||||
export const cmd = "/bin/bash";
|
||||
// Arguments, as an array (Required)
|
||||
export const args = ["--login"];
|
||||
// Optional extra parameters
|
||||
export const opts = {
|
||||
// Environment variables
|
||||
env: ["HOME=/home/user", "TERM=xterm", "USER=user", "SHELL=/bin/bash", "EDITOR=vim", "LANG=en_US.UTF-8", "LC_ALL=C"],
|
||||
// Current working directory
|
||||
cwd: "/home/user",
|
||||
// User id
|
||||
uid: 1000,
|
||||
// Group id
|
||||
gid: 1000
|
||||
};
|
1
dockerfiles/.dockerignore
Normal file
|
@ -0,0 +1 @@
|
|||
.dockerignore
|
19
dockerfiles/debian_large
Normal file
|
@ -0,0 +1,19 @@
|
|||
FROM --platform=i386 i386/debian:buster
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && apt-get -y upgrade && \
|
||||
apt-get install -y apt-utils beef bsdgames bsdmainutils ca-certificates clang \
|
||||
cowsay cpio cron curl dmidecode dmsetup g++ gcc gdbm-l10n git \
|
||||
hexedit ifupdown init logrotate lsb-base lshw lua50 luajit lynx make \
|
||||
nano netbase nodejs openssl procps python3 python3-cryptography \
|
||||
python3-jinja2 python3-numpy python3-pandas python3-pip python3-scipy \
|
||||
python3-six python3-yaml readline-common rsyslog ruby sensible-utils \
|
||||
ssh systemd systemd-sysv tasksel tasksel-data udev vim wget whiptail \
|
||||
xxd iptables isc-dhcp-client isc-dhcp-common kmod less netcat-openbsd
|
||||
|
||||
# Make a user, then copy over the /example directory
|
||||
RUN useradd -m user && echo "user:password" | chpasswd
|
||||
COPY --chown=user:user ./examples /home/user/examples
|
||||
RUN chmod -R +x /home/user/examples/lua
|
||||
RUN echo 'root:password' | chpasswd
|
||||
CMD [ "/bin/bash" ]
|
18
dockerfiles/debian_mini
Normal file
|
@ -0,0 +1,18 @@
|
|||
FROM --platform=i386 i386/debian:buster
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get clean && apt-get update && apt-get -y upgrade
|
||||
RUN apt-get -y install apt-utils gcc \
|
||||
python3 vim unzip ruby nodejs \
|
||||
fakeroot dbus base whiptail hexedit \
|
||||
patch wamerican ucf manpages \
|
||||
file luajit make lua50 dialog curl \
|
||||
less cowsay netcat-openbsd
|
||||
RUN useradd -m user && echo "user:password" | chpasswd
|
||||
COPY --chown=user:user ./examples /home/user/examples
|
||||
RUN chmod -R +x /home/user/examples/lua
|
||||
# We set WORKDIR, as this gets extracted by Webvm to be used as the cwd. This is optional.
|
||||
WORKDIR /home/user/
|
||||
# We set env, as this gets extracted by Webvm. This is optional.
|
||||
ENV HOME="/home/user" TERM="xterm" USER="user" SHELL="/bin/bash" EDITOR="vim" LANG="en_US.UTF-8" LC_ALL="C"
|
||||
RUN echo 'root:password' | chpasswd
|
||||
CMD [ "/bin/bash" ]
|
22
docs/Tailscale.md
Normal file
|
@ -0,0 +1,22 @@
|
|||
# Enable networking
|
||||
|
||||
- In order to access the public internet, you will need an Exit Node. See [Tailscale Exit Nodes](https://tailscale.com/kb/1103/exit-nodes/) for detailed instructions.
|
||||
- ***Note:*** This is not required to access machines in your own Tailscale Network.
|
||||
- Depending on your network speed, you may need to wait a few moments for the Tailscale Wasm module to be downloaded.
|
||||
|
||||
**When all set:**
|
||||
- Log in with your Tailscale credentials.
|
||||
- Go back to the WebVM tab.
|
||||
- The `Connect to Tailscale` button in the Networking side-panel should be replaced by your IP address.
|
||||
|
||||
# Log in to Tailscale with an Auth key
|
||||
|
||||
- Add `#authKey=<your-key>` at the end of the URL.
|
||||
- Done, you don't need to manually log in anymore.
|
||||
|
||||
It is recommended to use an ephemeral key.
|
||||
|
||||
# Log in to a self-hosted Tailscale network (Headscale)
|
||||
|
||||
- Add `#controlUrl=<your-control-url>` at the end of the URL.
|
||||
- You can combine this option with `authKey` with a `&`: `#controlUrl=<url>&authKey=<key>`.
|
BIN
documents/ArchitectureOverview.png
Normal file
After Width: | Height: | Size: 359 KiB |
BIN
documents/WebAssemblyTools.pdf
Normal file
5
documents/Welcome.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
Welcome to WebVM: A complete desktop environment running in the browser
|
||||
|
||||
WebVM is powered by CheerpX: a x86-to-WebAssembly virtualization engine and Just-in-Time compiler
|
||||
|
||||
For more info: https://cheerpx.io
|
3
documents/index.list
Normal file
|
@ -0,0 +1,3 @@
|
|||
ArchitectureOverview.png
|
||||
WebAssemblyTools.pdf
|
||||
Welcome.txt
|
|
@ -1,3 +1,4 @@
|
|||
#!/usr/bin/env luajit
|
||||
fruits = {"banana","orange","apple","grapes"}
|
||||
|
||||
for k,v in ipairs(fruits) do
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
#!/usr/bin/env luajit
|
||||
A = { ["John"] = true, ["Bob"] = true, ["Mary"] = true, ["Elena"] = true }
|
||||
B = { ["Jim"] = true, ["Mary"] = true, ["John"] = true, ["Bob"] = true }
|
||||
|
||||
|
|
BIN
favicon.ico
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 73 KiB |
317
index.html
|
@ -1,317 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" style="height:100%;">
|
||||
<head>
|
||||
<!-- Google Tag Manager -->
|
||||
<script>
|
||||
(function (w, d, s, l, i) {w[l] = w[l] || []; w[l].push({'gtm.start': new Date().getTime(),event: 'gtm.js'});var f = d.getElementsByTagName(s)[0],j = d.createElement(s),dl = l != 'dataLayer' ? '&l=' + l : '';j.async = true;j.src ='https://www.googletagmanager.com/gtm.js?id=' + i + dl;f.parentNode.insertBefore(j, f);})(window, document, 'script', 'dataLayer', 'GTM-5GBJM42');
|
||||
</script>
|
||||
<!-- End Google Tag Manager -->
|
||||
|
||||
<meta charset="utf-8">
|
||||
<title>WebVM</title>
|
||||
|
||||
<meta name="description" content="Welcome to WebVM - a server-less virtual Linux environment running fully client-side in HTML5/WebAssembly."
|
||||
<meta name="keywords" content="WebVM, Bash, CheerpX, WebAssembly">
|
||||
<meta property="og:title" content="WebVM" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="WebVM"/>
|
||||
<meta property="og:url" content="/">
|
||||
<meta property="og:image" content="https://webvm.io/assets/webvm.jpeg" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:site" content="@leaningtech" />
|
||||
<meta name="twitter:title" content="WebVM" />
|
||||
<meta name="twitter:description" content="Welcome to WebVM - a server-less virtual Linux environment running fully client-side in HTML5/WebAssembly."
|
||||
<meta name="twitter:image" content="https://webvm.io/assets/webvm.jpeg" />
|
||||
|
||||
<!-- Apple iOS web clip compatibility tags -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="application-name" content="WebVM" />
|
||||
<meta name="apple-mobile-web-app-title" content="WebVM" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
|
||||
|
||||
<link rel="shortcut icon" href="/favicon.ico">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/xterm/xterm.css" />
|
||||
<script src="/xterm/xterm.js"></script>
|
||||
<script src="/xterm/xterm-addon-fit.js"></script>
|
||||
</head>
|
||||
|
||||
<body style="margin:0;height:100%;background:black;color:white;overflow:hidden;">
|
||||
<!-- Google Tag Manager (noscript) -->
|
||||
<noscript>
|
||||
<iframe src="https://www.googletagmanager.com/ns.html?id=GTM-5GBJM42" height="0" width="0"
|
||||
style="display:none;visibility:hidden"></iframe>
|
||||
</noscript>
|
||||
<!-- End Google Tag Manager (noscript) -->
|
||||
|
||||
<main style="display: flex; flex-direction: row; justify-content: space-between; margin:0; height:100%;">
|
||||
<div style="flex-grow:1; height:100%;display:inline-block;margin:0;" id="console">
|
||||
</div>
|
||||
|
||||
</main>
|
||||
<script>
|
||||
|
||||
//Utility namespace to group all functionality related to printing (both error and non error) messages
|
||||
const color= "\x1b[1;35m";
|
||||
const bold= "\x1b[1;37m";
|
||||
const underline= "\x1b[94;4m";
|
||||
const normal= "\x1b[0m";
|
||||
var printOnTerm = {
|
||||
getAsciiTitle: function ()
|
||||
{
|
||||
var title = [
|
||||
color + " __ __ _ __ ____ __ " + normal,
|
||||
color + " \\ \\ / /__| |_\\ \\ / / \\/ | " + normal,
|
||||
color + " \\ \\/\\/ / -_) '_ \\ V /| |\\/| | " + normal,
|
||||
color + " \\_/\\_/\\___|_.__/\\_/ |_| |_| " + normal,
|
||||
|
||||
];
|
||||
return title;
|
||||
},
|
||||
getAsciiText: function ()
|
||||
{
|
||||
var text = [
|
||||
" +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+",
|
||||
" | |",
|
||||
" | WebVM is a server-less virtual Linux environment running fully client-side |",
|
||||
" | in HTML5/WebAssembly. |",
|
||||
" | |",
|
||||
" | In this demo, it runs an unmodified Debian distribution including many |",
|
||||
" | native development toolchains. |",
|
||||
" | |",
|
||||
" | WebVM is powered by the CheerpX virtualization engine, and enables safe, |",
|
||||
" | sandboxed client-side execution of x86 binaries on any browser. |",
|
||||
" | |",
|
||||
" | CheerpX includes an x86-to-WebAssembly JIT compiler, a virtual block-based |",
|
||||
" | file system, and a Linux syscall emulator. |",
|
||||
" | |",
|
||||
" | For more information: " + underline + "https://medium.com/p/40a60170b361" + normal + " |",
|
||||
" | |",
|
||||
" | " +
|
||||
underline + "GitHub" + normal + " | " +
|
||||
underline + "Issues" + normal + " | " +
|
||||
underline + "Gitter" + normal + " | " +
|
||||
underline + "Twitter" + normal + " | " +
|
||||
underline + "Latest News" + normal + " | " +
|
||||
underline + "About CheerpX" + normal + " |",
|
||||
" | |",
|
||||
" +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+",
|
||||
"",
|
||||
" Welcome to WebVM (build CX_VERSION). If unsure, try these examples:",
|
||||
"",
|
||||
" python3 examples/python3/fibonacci.py ",
|
||||
" gcc -o helloworld examples/c/helloworld.c && ./helloworld",
|
||||
" objdump -d ./helloworld | less -M",
|
||||
" vim examples/c/helloworld.c",
|
||||
"",
|
||||
];
|
||||
return text;
|
||||
},
|
||||
getSharedArrayBufferMissingMessage: function ()
|
||||
{
|
||||
const text = [
|
||||
"",
|
||||
"",
|
||||
color + "CheerpX could not start" + normal,
|
||||
"",
|
||||
"CheerpX depends on JavaScript's SharedArrayBuffer, that your browser",
|
||||
" does not support.",
|
||||
"",
|
||||
"SharedArrayBuffer is currently enabled by default on recent",
|
||||
" versions of Chrome, Edge, Firefox and Safari.",
|
||||
"",
|
||||
"",
|
||||
"Give it a try from another browser!",
|
||||
]
|
||||
|
||||
return text;
|
||||
},
|
||||
getErrorMessage: function (error_message)
|
||||
{
|
||||
const text = [
|
||||
"",
|
||||
"",
|
||||
color + "CheerpX could not start" + normal,
|
||||
"",
|
||||
"CheerpX internal error message is:",
|
||||
error_message,
|
||||
"",
|
||||
"",
|
||||
"CheerpX is expected to work with recent desktop versions of Chrome, Edge, Firefox and Safari",
|
||||
"",
|
||||
"",
|
||||
"Give it a try from a desktop version / another browser!",
|
||||
]
|
||||
|
||||
return text;
|
||||
},
|
||||
printMessage: function (text) {
|
||||
for (var i=0; i<text.length; i++)
|
||||
{
|
||||
term.write(text[i]);
|
||||
term.write('\n');
|
||||
}
|
||||
},
|
||||
printError: function (message)
|
||||
{
|
||||
this.printMessage(message);
|
||||
|
||||
term.write("\n\n");
|
||||
|
||||
function writeCustom(something)
|
||||
{
|
||||
term.write(something);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
var consoleDiv = document.getElementById("console");
|
||||
|
||||
//xterm.js related logic
|
||||
var term = new Terminal({cursorBlink:true,convertEol:true, fontFamily:"'Roboto Mono', monospace", fontWeight: 400, fontWeightBold: 700});
|
||||
var fitAddon = new FitAddon.FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
term.open(consoleDiv);
|
||||
term.scrollToTop();
|
||||
|
||||
fitAddon.fit();
|
||||
window.addEventListener("resize", function(ev){fitAddon.fit();}, false);
|
||||
term.focus();
|
||||
var cxReadFunc = null;
|
||||
function writeData(buf)
|
||||
{
|
||||
term.write(new Uint8Array(buf));
|
||||
}
|
||||
function readData(str)
|
||||
{
|
||||
if(cxReadFunc == null)
|
||||
return;
|
||||
for(var i=0;i<str.length;i++)
|
||||
cxReadFunc(str.charCodeAt(i));
|
||||
}
|
||||
term.onData(readData);
|
||||
|
||||
//Actual CheerpX and bash specific logic
|
||||
function runBash()
|
||||
{
|
||||
const structure = {
|
||||
name: "bash",
|
||||
cmd: "/bin/bash",
|
||||
args: ["--login"],
|
||||
env: ["HOME=/home/user", "TERM=xterm", "USER=user", "SHELL=/bin/bash", "EDITOR=vim", "LANG=en_US.UTF-8", "LC_ALL=C"],
|
||||
expectedPrompt: ">",
|
||||
versionOpt: "--version",
|
||||
comment_line: "#",
|
||||
description_line: "The original Bourne Again SHell",
|
||||
}
|
||||
if (typeof SharedArrayBuffer === "undefined")
|
||||
{
|
||||
printOnTerm.printError(printOnTerm.getSharedArrayBufferMissingMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
async function runTest(cx)
|
||||
{
|
||||
term.scrollToBottom();
|
||||
|
||||
async function cxLogAndRun(cheerpx, cmd, args, env)
|
||||
{
|
||||
await cheerpx.run(cmd, args, env);
|
||||
printOnTerm.printMessage(" ");
|
||||
}
|
||||
|
||||
cxReadFunc = cx.setCustomConsole(writeData, term.cols, term.rows);
|
||||
|
||||
function preventDefaults (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
consoleDiv.addEventListener("dragover", preventDefaults, false);
|
||||
consoleDiv.addEventListener("dragenter", preventDefaults, false);
|
||||
consoleDiv.addEventListener("dragleave", preventDefaults, false);
|
||||
consoleDiv.addEventListener("drop", preventDefaults, false);
|
||||
|
||||
var opts = {env:structure.env, cwd:"/home/user"};
|
||||
while (true)
|
||||
{
|
||||
await cxLogAndRun(cx, structure.cmd, structure.args, opts);
|
||||
}
|
||||
}
|
||||
function failCallback(err)
|
||||
{
|
||||
printOnTerm.printError(printOnTerm.getErrorMessage(err));
|
||||
}
|
||||
CheerpXApp.create({devices:[{type:"block",url:"https://disks.leaningtech.com/webvm_20220131.ext2",name:"block1"}],mounts:[{type:"ext2",dev:"block1",path:"/"},{type:"cheerpOS",dev:"/app",path:"/bootstrap"},{type:"devs",dev:"",path:"/dev"}]}).then(runTest, failCallback);
|
||||
}
|
||||
function initialMessage()
|
||||
{
|
||||
printOnTerm.printMessage(printOnTerm.getAsciiTitle());
|
||||
printOnTerm.printMessage(["\n"]);
|
||||
printOnTerm.printMessage(printOnTerm.getAsciiText());
|
||||
term.registerLinkMatcher(/https:\/\/medium\.com\/p\/40a60170b361/, function(mouseEvent, matchedString) {
|
||||
window.open(matchedString, "_blank")
|
||||
});
|
||||
const textArray = new Array(6);
|
||||
const linksArray = new Array(6);
|
||||
const rangesArray = new Array(6);
|
||||
var last = 0;
|
||||
const textLinkLine = " | GitHub | Issues | Gitter | Twitter | Latest News | About CheerpX |";
|
||||
const lineWithLinks = 23;
|
||||
function addLink(text, website)
|
||||
{
|
||||
var index = textLinkLine.indexOf(text);
|
||||
const start_x = index+1;
|
||||
const end_x = index + text.length;
|
||||
rangesArray[last] = {start: {x:start_x, y:lineWithLinks}, end: {x:end_x, y:lineWithLinks}};
|
||||
linksArray[last] = website;
|
||||
last++;
|
||||
}
|
||||
|
||||
addLink("GitHub", "https://github.com/leaningtech/webvm");
|
||||
addLink("Issues", "https://github.com/leaningtech/webvm/issues");
|
||||
addLink("Gitter", "https://gitter.im/leaningtech/cheerpx");
|
||||
addLink("Twitter", "https://twitter.com/leaningtech");
|
||||
addLink("Latest News", "https://medium.com/leaningtech");
|
||||
addLink("About CheerpX", "https://leaningtech.com/cheerpx");
|
||||
var provider = {
|
||||
provideLinks(bufferLineNum, callback) {
|
||||
switch(bufferLineNum)
|
||||
{
|
||||
case lineWithLinks:
|
||||
{
|
||||
callback([
|
||||
{range: rangesArray[0], activate() {window.open('' + linksArray[0], '_blank');}},
|
||||
{range: rangesArray[1], activate() {window.open('' + linksArray[1], '_blank');}},
|
||||
{range: rangesArray[2], activate() {window.open('' + linksArray[2], '_blank');}},
|
||||
{range: rangesArray[3], activate() {window.open('' + linksArray[3], '_blank');}},
|
||||
{range: rangesArray[4], activate() {window.open('' + linksArray[4], '_blank');}},
|
||||
{range: rangesArray[5], activate() {window.open('' + linksArray[5], '_blank');}},
|
||||
]);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
term.registerLinkProvider(provider);
|
||||
console.log("Welcome. We appreciate curiosity, but be warned that keeping the DevTools open causes significant performance degradation and crashes.");
|
||||
}
|
||||
initialMessage();
|
||||
|
||||
var script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
|
||||
var cxFile = "https://cheerpxdemos.leaningtech.com/publicdeploy/CX_VERSION/cx.js";
|
||||
script.src = cxFile;
|
||||
script.addEventListener("load", runBash, false);
|
||||
|
||||
document.head.appendChild(script);
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
12
login.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Tailscale login</title>
|
||||
</head>
|
||||
<body>
|
||||
Loading network code...
|
||||
</body>
|
||||
</html>
|
35
nginx.conf
|
@ -32,45 +32,12 @@ http {
|
|||
# ssl_certificate_key nginx.key;
|
||||
|
||||
location / {
|
||||
root .;
|
||||
root build;
|
||||
autoindex on;
|
||||
index index.html index.htm;
|
||||
add_header 'Access-Control-Allow-Origin' '*' always;
|
||||
add_header 'Access-Control-Expose-Headers' 'content-length' always;
|
||||
add_header 'Cross-Origin-Opener-Policy' 'same-origin' always;
|
||||
add_header 'Cross-Origin-Embedder-Policy' 'require-corp' always;
|
||||
add_header 'Cross-Origin-Resource-Policy' 'cross-origin' always;
|
||||
#auth_basic "CX Demo";
|
||||
#auth_basic_user_file basicauth;
|
||||
}
|
||||
|
||||
location /images/ {
|
||||
root .;
|
||||
if ($arg_s != "") {
|
||||
rewrite ^/images/(.*)$ $1 break;
|
||||
}
|
||||
if ($arg_s = "") {
|
||||
gzip off;
|
||||
}
|
||||
error_page 404 =200 /images_slicer/$uri?$args;
|
||||
}
|
||||
|
||||
location /images_slicer/ {
|
||||
proxy_pass http://localhost:8082/images/;
|
||||
proxy_http_version 1.0;
|
||||
proxy_set_header Range bytes=$arg_s-$arg_e;
|
||||
proxy_hide_header Content-Range;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 127.0.0.1:8082;
|
||||
server_name localhost;
|
||||
|
||||
charset utf-8;
|
||||
|
||||
location / {
|
||||
root .;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
3026
package-lock.json
generated
Normal file
31
package.json
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "webvm",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||
"@leaningtech/cheerpx": "latest",
|
||||
"@oddbird/popover-polyfill": "^0.4.4",
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.5",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"labs": "git@github.com:leaningtech/labs.git",
|
||||
"node-html-parser": "^6.1.13",
|
||||
"postcss": "^8.4.47",
|
||||
"postcss-discard": "^2.0.0",
|
||||
"svelte": "^4.2.7",
|
||||
"tailwindcss": "^3.4.9",
|
||||
"vite": "^5.0.3",
|
||||
"vite-plugin-static-copy": "^1.0.6"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
26
postcss.config.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
'postcss-discard': {rule: function(node, value)
|
||||
{
|
||||
if(!value.startsWith('.fa-') || !value.endsWith(":before"))
|
||||
return false;
|
||||
switch(value)
|
||||
{
|
||||
case '.fa-info-circle:before':
|
||||
case '.fa-wifi:before':
|
||||
case '.fa-microchip:before':
|
||||
case '.fa-compact-disc:before':
|
||||
case '.fa-discord:before':
|
||||
case '.fa-github:before':
|
||||
case '.fa-star:before':
|
||||
case '.fa-circle:before':
|
||||
case '.fa-trash-can:before':
|
||||
case '.fa-book-open:before':
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}}
|
||||
},
|
||||
}
|
21
scrollbar.css
Normal file
|
@ -0,0 +1,21 @@
|
|||
.scrollbar {
|
||||
scrollbar-color: #777 #0000;
|
||||
}
|
||||
|
||||
.scrollbar *::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
background-color: #0000;
|
||||
}
|
||||
|
||||
/* Add a thumb */
|
||||
.scrollbar *::-webkit-scrollbar-thumb {
|
||||
border-radius: 3px;
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
background: #777;
|
||||
}
|
||||
|
||||
.scrollbar *::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
96
serviceWorker.js
Normal file
|
@ -0,0 +1,96 @@
|
|||
async function handleFetch(request) {
|
||||
// Perform the original fetch request and store the result in order to modify the response.
|
||||
try {
|
||||
var r = await fetch(request);
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
if (r.status === 0) {
|
||||
return r;
|
||||
}
|
||||
// We add headers to the original response its headers, in order to enable cross-origin-isolation. And make it independent of the server config.
|
||||
const newHeaders = new Headers(r.headers);
|
||||
// COEP & COOP for cross-origin-isolation.
|
||||
newHeaders.set("Cross-Origin-Embedder-Policy", "require-corp");
|
||||
newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
|
||||
newHeaders.set("Cross-Origin-Resource-Policy", "cross-origin");
|
||||
/**
|
||||
* This workaround is necessary due to a limitation of CheerpOS, which relies on the response URL being set to the resolved URL.
|
||||
* When constructing a new response object, the URL is not set by the Response() constructor and the serviceworker respondwith() method will set the url to event.request.url in case of an empty string.
|
||||
* To address this, we set the location URL to the resolved response URL and set the status code to 301 in the new Response object.
|
||||
* This causes the request to bounce back to the serviceworker from Cheerpos, with the event.request.url now set to the resolved URL, which allows the respondWith method to properly set the response URL in our new response.
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent/respondWith.
|
||||
*/
|
||||
if (r.redirected === true)
|
||||
newHeaders.set("location", r.url);
|
||||
// In case of a redirection, we set the status to 301, and body to null, in order to not transfer too much data needlessly
|
||||
const moddedResponse = new Response(r.redirected === true ? null : r.body, {
|
||||
headers: newHeaders,
|
||||
status: r.redirected === true ? 301 : r.status,
|
||||
statusText: r.statusText,
|
||||
});
|
||||
return moddedResponse;
|
||||
}
|
||||
|
||||
function serviceWorkerInit() {
|
||||
// Init the service worker.
|
||||
self.addEventListener("install", () => self.skipWaiting());
|
||||
self.addEventListener("activate", e => e.waitUntil(self.clients.claim()));
|
||||
// Listen for fetch requests and call handleFetch function.
|
||||
self.addEventListener("fetch", function (e) {
|
||||
try {
|
||||
e.respondWith(handleFetch(e.request));
|
||||
} catch (err) {
|
||||
console.log("Serviceworker NetworkError:" + err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function doRegister() {
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.register(window.document.currentScript.src);
|
||||
console.log("Service Worker registered", registration.scope);
|
||||
// EventListener to make sure that the page gets reloaded when a new serviceworker gets installed.
|
||||
// f.e on first access.
|
||||
registration.addEventListener("updatefound", () => {
|
||||
console.log("Reloading the page to transfer control to the Service Worker.");
|
||||
try {
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
console.log("Service Worker failed reloading the page. ERROR:" + err);
|
||||
};
|
||||
});
|
||||
// When the registration is active, but it's not controlling the page, we reload the page to have it take control.
|
||||
// This f.e occurs when you hard-reload (shift + refresh). https://www.w3.org/TR/service-workers/#navigator-service-worker-controller
|
||||
if (registration.active && !navigator.serviceWorker.controller) {
|
||||
console.log("Reloading the page to transfer control to the Service Worker.");
|
||||
try {
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
console.log("Service Worker failed reloading the page. ERROR:" + err);
|
||||
};
|
||||
}
|
||||
}
|
||||
catch {
|
||||
console.error("Service Worker failed to register:", e)
|
||||
}
|
||||
}
|
||||
|
||||
async function serviceWorkerRegister() {
|
||||
if (window.crossOriginIsolated) return;
|
||||
if (!window.isSecureContext) {
|
||||
console.log("Service Worker not registered, a secure context is required.");
|
||||
return;
|
||||
}
|
||||
// Register the service worker and reload the page to transfer control to the serviceworker.
|
||||
if ("serviceWorker" in navigator)
|
||||
await doRegister();
|
||||
else
|
||||
console.log("Service worker is not supported in this browser");
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined') // If the script is running in a Service Worker context
|
||||
serviceWorkerInit()
|
||||
else // If the script is running in the browser context
|
||||
serviceWorkerRegister();
|
38
src/app.html
Normal file
|
@ -0,0 +1,38 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>WebVM - Linux virtualization in WebAssembly</title>
|
||||
|
||||
<meta name="description" content="Linux virtual machine, running in the browser via HTML5/WebAssembly. Networking and graphics supported.">
|
||||
<meta name="keywords" content="WebVM, Virtual Machine, CheerpX, x86 virtualization, WebAssembly, Tailscale, JIT">
|
||||
<meta property="og:title" content="WebVM - Linux virtualization in WebAssembly" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="WebVM"/>
|
||||
<meta property="og:image" content="https://webvm.io/assets/social_2024.png" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="@leaningtech" />
|
||||
<meta name="twitter:title" content="WebVM - Linux virtualization in WebAssembly" />
|
||||
<meta name="twitter:description" content="Linux virtual machine, running in the browser via HTML5/WebAssembly. Networking and graphics supported.">
|
||||
<meta name="twitter:image" content="https://webvm.io/assets/social_2024.png" />
|
||||
|
||||
<!-- Apple iOS web clip compatibility tags -->
|
||||
<meta name="application-name" content="WebVM" />
|
||||
<meta name="apple-mobile-web-app-title" content="WebVM" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
<link rel="shortcut icon" href="tower.ico">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel='stylesheet' href='scrollbar.css'>
|
||||
<!-- Serviceworker script that adds the COI headers to the response headers in cases where the server does not support it. -->
|
||||
<script src="serviceWorker.js"></script>
|
||||
<script data-domain="webvm.io" src="https://plausible.leaningtech.com/js/script.js"></script>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
9
src/lib/BlogPost.svelte
Normal file
|
@ -0,0 +1,9 @@
|
|||
<script>
|
||||
export let title;
|
||||
export let image;
|
||||
export let url;
|
||||
</script>
|
||||
<a href={url} target="_blank"><div class="bg-neutral-700 hover:bg-neutral-500 p-2 rounded-md">
|
||||
<img class="w-56 h-32 object-fit" src={image}>
|
||||
<h2 class="text-sm font-bold">{title}</h2>
|
||||
</div></a>
|
12
src/lib/CpuTab.svelte
Normal file
|
@ -0,0 +1,12 @@
|
|||
<script>
|
||||
import PanelButton from './PanelButton.svelte';
|
||||
import { cpuPercentage } from './activities.js'
|
||||
</script>
|
||||
|
||||
<h1 class="text-lg font-bold">Engine</h1>
|
||||
<PanelButton buttonImage="assets/cheerpx.svg" clickUrl="https://cheerpx.io/docs" buttonText="Explore CheerpX">
|
||||
</PanelButton>
|
||||
<p><span class="font-bold">Virtual CPU: </span>{$cpuPercentage}%</p>
|
||||
<p>CheerpX is a x86 virtualization engine in WebAssembly</p>
|
||||
<p>It can securely run unmodified x86 binaries and libraries in the browser</p>
|
||||
<p>Excited about our technology? <a class="underline" href="https://cheerpx.io/docs/getting-started" target="_blank">Start building</a> your projects using <a class="underline" href="https://cheerpx.io/" target="_blank">CheerpX</a> today!</p>
|
12
src/lib/DiscordTab.svelte
Normal file
|
@ -0,0 +1,12 @@
|
|||
<script>
|
||||
import PanelButton from './PanelButton.svelte';
|
||||
import DiscordPresenceCount from 'labs/packages/astro-theme/components/nav/DiscordPresenceCount.svelte'
|
||||
</script>
|
||||
|
||||
<h1 class="text-lg font-bold">Discord</h1>
|
||||
<PanelButton buttonImage="assets/discord-mark-blue.svg" clickUrl="https://discord.gg/yTNZgySKGa" buttonText="Join our Discord">
|
||||
<i class='fas fa-circle fa-xs ml-auto text-green-500'></i>
|
||||
<span class="ml-1"><DiscordPresenceCount /></span>
|
||||
</PanelButton>
|
||||
<p>Do you have any question about WebVM or CheerpX?</p>
|
||||
<p>Join our community, we are happy to help!</p>
|
56
src/lib/DiskTab.svelte
Normal file
|
@ -0,0 +1,56 @@
|
|||
<script>
|
||||
import PanelButton from './PanelButton.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { diskLatency } from './activities.js'
|
||||
var dispatch = createEventDispatcher();
|
||||
let state = "START";
|
||||
function handleReset()
|
||||
{
|
||||
if(state == "START")
|
||||
state = "CONFIRM";
|
||||
else
|
||||
dispatch('reset');
|
||||
}
|
||||
function getButtonText(state)
|
||||
{
|
||||
if(state == "START")
|
||||
return "Reset disk";
|
||||
else
|
||||
return "Reset disk. Confirm?"
|
||||
}
|
||||
function getBgColor(state)
|
||||
{
|
||||
if(state == "START")
|
||||
{
|
||||
// Use default
|
||||
return undefined;
|
||||
}
|
||||
else
|
||||
{
|
||||
return "bg-red-900";
|
||||
}
|
||||
}
|
||||
function getHoverColor(state)
|
||||
{
|
||||
if(state == "START")
|
||||
{
|
||||
// Use default
|
||||
return undefined;
|
||||
}
|
||||
else
|
||||
{
|
||||
return "hover:bg-red-700";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<h1 class="text-lg font-bold">Disk</h1>
|
||||
<PanelButton buttonIcon="fa-solid fa-trash-can" clickHandler={handleReset} buttonText={getButtonText(state)} bgColor={getBgColor(state)} hoverColor={getHoverColor(state)}>
|
||||
</PanelButton>
|
||||
{#if state == "CONFIRM"}
|
||||
<p><span class="font-bold">Warning: </span>WebVM will reload</p>
|
||||
{:else}
|
||||
<p><span class="font-bold">Backend latency: </span>{$diskLatency}ms</p>
|
||||
{/if}
|
||||
<p>WebVM runs on top of a complete Linux distribution</p>
|
||||
<p>Filesystems up to 2GB are supported and data is downloaded completely on-demand</p>
|
||||
<p>The WebVM cloud backend uses WebSockets and a it's distributed via a global CDN to minimize download latency</p>
|
13
src/lib/GitHubTab.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<script>
|
||||
import PanelButton from './PanelButton.svelte';
|
||||
import GitHubStarCount from 'labs/packages/astro-theme/components/nav/GitHubStarCount.svelte'
|
||||
</script>
|
||||
|
||||
<h1 class="text-lg font-bold">GitHub</h1>
|
||||
<PanelButton buttonImage="assets/github-mark-white.svg" clickUrl="https://github.com/leaningtech/webvm" buttonText="GitHub repo">
|
||||
<i class='fas fa-star fa-xs ml-auto'></i>
|
||||
<span class="ml-1"><GitHubStarCount repo="leaningtech/webvm"/></span>
|
||||
</PanelButton>
|
||||
<p>Like WebVM? <a class="underline" href="https://github.com/leaningtech/webvm" target="_blank">Give us a star!</a></p>
|
||||
<p>WebVM is FOSS, you can fork it to build your own version and begin working on your CheerpX-based project</p>
|
||||
<p>Found a bug? Please open a <a class="underline" href="https://github.com/leaningtech/webvm/issues" target="_blank">GitHub issue</a></p>
|
20
src/lib/Icon.svelte
Normal file
|
@ -0,0 +1,20 @@
|
|||
<script>
|
||||
export let icon;
|
||||
export let info;
|
||||
export let activity;
|
||||
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function handleMouseover() {
|
||||
dispatch('mouseover', info);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="p-3 cursor-pointer text-center hover:bg-neutral-600 {$activity ? "text-amber-500 animate-pulse" : "hover:text-gray-100"}"
|
||||
style="animation-duration: 0.5s"
|
||||
on:mouseenter={handleMouseover}
|
||||
>
|
||||
<i class='{icon} fa-xl'></i>
|
||||
</div>
|
11
src/lib/InformationTab.svelte
Normal file
|
@ -0,0 +1,11 @@
|
|||
<h1 class="text-lg font-bold">Information</h1>
|
||||
<img src="assets/webvm_hero.png" alt="WebVM Logo" class="w-56 h-56 object-contain self-center">
|
||||
<p>WebVM is a virtual Linux environment running in the browser via WebAssembly</p>
|
||||
<p>It is based on:</p>
|
||||
<ul class="list-disc list-inside">
|
||||
<li><a class="underline" target="_blank" href="https://cheerpx.io/">CheerpX</a>: x86 JIT in Wasm</li>
|
||||
<li><a class="underline" target="_blank" href="https://xtermjs.org/">Xterm.js</a>: interactive terminal</li>
|
||||
<li>Local/private <a class="underline" target="_blank" href="https://cheerpx.io/docs/guides/File-System-support">file storage</a></li>
|
||||
<li><a class="underline" target="_blank" href="https://cheerpx.io/docs/guides/Networking">Networking</a> via <a class="underline" target="_blank" href="https://tailscale.com/">Tailscale</a></li>
|
||||
</ul>
|
||||
<slot></slot>
|
108
src/lib/NetworkingTab.svelte
Normal file
|
@ -0,0 +1,108 @@
|
|||
<script>
|
||||
import { networkData, startLogin } from '$lib/network.js'
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import PanelButton from './PanelButton.svelte';
|
||||
var dispatch = createEventDispatcher();
|
||||
var connectionState = networkData.connectionState;
|
||||
var exitNode = networkData.exitNode;
|
||||
function handleConnect() {
|
||||
connectionState.set("DOWNLOADING");
|
||||
dispatch('connect');
|
||||
}
|
||||
async function handleCopyIP(event)
|
||||
{
|
||||
// To prevent the default contexmenu from showing up when right-clicking..
|
||||
event.preventDefault();
|
||||
// Copy the IP to the clipboard.
|
||||
try
|
||||
{
|
||||
await window.navigator.clipboard.writeText(networkData.currentIp)
|
||||
connectionState.set("IPCOPIED");
|
||||
setTimeout(() => {
|
||||
connectionState.set("CONNECTED");
|
||||
}, 2000);
|
||||
}
|
||||
catch(msg)
|
||||
{
|
||||
console.log("Copy ip to clipboard: Error: " + msg);
|
||||
}
|
||||
}
|
||||
function getButtonText(state)
|
||||
{
|
||||
switch(state)
|
||||
{
|
||||
case "DISCONNECTED":
|
||||
return "Connect to Tailscale";
|
||||
case "DOWNLOADING":
|
||||
return "Loading IP stack...";
|
||||
case "LOGINSTARTING":
|
||||
return "Starting Login...";
|
||||
case "LOGINREADY":
|
||||
return "Login to Tailscale";
|
||||
case "CONNECTED":
|
||||
return `IP: ${networkData.currentIp}`;
|
||||
case "IPCOPIED":
|
||||
return "Copied!";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return `Text for state: ${state}`;
|
||||
}
|
||||
function isClickableState(state)
|
||||
{
|
||||
switch(state)
|
||||
{
|
||||
case "DISCONNECTED":
|
||||
case "LOGINREADY":
|
||||
case "CONNECTED":
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function getClickHandler(state)
|
||||
{
|
||||
switch(state)
|
||||
{
|
||||
case "DISCONNECTED":
|
||||
return handleConnect;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function getClickUrl(state)
|
||||
{
|
||||
switch(state)
|
||||
{
|
||||
case "LOGINREADY":
|
||||
return networkData.loginUrl;
|
||||
case "CONNECTED":
|
||||
return networkData.dashboardUrl;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function getButtonTooltip(state)
|
||||
{
|
||||
switch(state)
|
||||
{
|
||||
case "CONNECTED":
|
||||
return "Right-click to copy";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function getRightClickHandler(state)
|
||||
{
|
||||
switch(state)
|
||||
{
|
||||
case "CONNECTED":
|
||||
return handleCopyIP;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
</script>
|
||||
<h1 class="text-lg font-bold">Networking</h1>
|
||||
<PanelButton buttonImage="assets/tailscale.svg" clickUrl={getClickUrl($connectionState)} clickHandler={getClickHandler($connectionState)} rightClickHandler={getRightClickHandler($connectionState)} buttonTooltip={getButtonTooltip($connectionState)} buttonText={getButtonText($connectionState)}>
|
||||
{#if $connectionState == "CONNECTED"}
|
||||
<i class='fas fa-circle fa-xs ml-auto {$exitNode ? 'text-green-500' : 'text-amber-500'}' title={$exitNode ? 'Ready' : 'No exit node'}></i>
|
||||
{/if}
|
||||
</PanelButton>
|
||||
<p>WebVM can connect to the Internet via Tailscale</p>
|
||||
<p>Using Tailscale is required since browser do not support TCP/UDP sockets (yet!)</p>
|
19
src/lib/PanelButton.svelte
Normal file
|
@ -0,0 +1,19 @@
|
|||
<script>
|
||||
export let clickUrl = null;
|
||||
export let clickHandler = null;
|
||||
export let rightClickHandler = null;
|
||||
export let buttonTooltip = null;
|
||||
export let bgColor = "bg-neutral-700";
|
||||
export let hoverColor = "hover:bg-neutral-500"
|
||||
export let buttonImage = null;
|
||||
export let buttonIcon = null;
|
||||
export let buttonText;
|
||||
</script>
|
||||
|
||||
<a href={clickUrl} target="_blank" on:click={clickHandler} on:contextmenu={rightClickHandler}><p class="flex flex-row items-center {bgColor} p-2 rounded-md shadow-md shadow-neutral-900 {(clickUrl != null || clickHandler != null) ? `${hoverColor} cursor-pointer` : ""}" title={buttonTooltip}>
|
||||
{#if buttonImage}
|
||||
<img src={buttonImage} class="inline w-8 h-8"/>
|
||||
{:else if buttonIcon}
|
||||
<i class="w-8 {buttonIcon} text-center" style="font-size: 2em;"></i>
|
||||
{/if}
|
||||
<span class="ml-1">{buttonText}</span><slot></slot></p></a>
|
14
src/lib/PostsTab.svelte
Normal file
|
@ -0,0 +1,14 @@
|
|||
<script>
|
||||
import BlogPost from './BlogPost.svelte';
|
||||
import { page } from '$app/stores';
|
||||
</script>
|
||||
<h1 class="text-lg font-bold">Blog posts</h1>
|
||||
<div class="overflow-y-scroll scrollbar flex flex-col gap-2">
|
||||
{#each $page.data.posts as post}
|
||||
<BlogPost
|
||||
title={post.title}
|
||||
image={post.image}
|
||||
url={post.url}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
82
src/lib/SideBar.svelte
Normal file
|
@ -0,0 +1,82 @@
|
|||
<script>
|
||||
import Icon from './Icon.svelte';
|
||||
import InformationTab from './InformationTab.svelte';
|
||||
import NetworkingTab from './NetworkingTab.svelte';
|
||||
import CpuTab from './CpuTab.svelte';
|
||||
import DiskTab from './DiskTab.svelte';
|
||||
import PostsTab from './PostsTab.svelte';
|
||||
import DiscordTab from './DiscordTab.svelte';
|
||||
import GitHubTab from './GitHubTab.svelte';
|
||||
import { cpuActivity, diskActivity } from './activities.js'
|
||||
|
||||
const icons = [
|
||||
{ icon: 'fas fa-info-circle', info: 'Information', activity: null },
|
||||
{ icon: 'fas fa-wifi', info: 'Networking', activity: null },
|
||||
{ icon: 'fas fa-microchip', info: 'CPU', activity: cpuActivity },
|
||||
{ icon: 'fas fa-compact-disc', info: 'Disk', activity: diskActivity },
|
||||
null,
|
||||
{ icon: 'fas fa-book-open', info: 'Posts', activity: null },
|
||||
{ icon: 'fab fa-discord', info: 'Discord', activity: null },
|
||||
{ icon: 'fab fa-github', info: 'GitHub', activity: null },
|
||||
];
|
||||
|
||||
let activeInfo = null;
|
||||
|
||||
function showInfo(info) {
|
||||
activeInfo = info;
|
||||
}
|
||||
|
||||
function hideInfo() {
|
||||
activeInfo = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row w-14 h-full bg-neutral-700" on:mouseleave={hideInfo}>
|
||||
<div class="flex flex-col shrink-0 w-14 text-gray-300">
|
||||
{#each icons as i}
|
||||
{#if i}
|
||||
<Icon
|
||||
icon={i.icon}
|
||||
info={i.info}
|
||||
activity={i.activity}
|
||||
on:mouseover={(e) => showInfo(e.detail)}
|
||||
/>
|
||||
{:else}
|
||||
<div class="grow" on:mouseover={(e) => showInfo(null)}></div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex flex-col gap-5 shrink-0 w-60 h-full z-10 p-2 bg-neutral-600 text-gray-100" class:hidden={!activeInfo}>
|
||||
{#if activeInfo === 'Information'}
|
||||
<InformationTab>
|
||||
<slot></slot>
|
||||
</InformationTab>
|
||||
{:else if activeInfo === 'Networking'}
|
||||
<NetworkingTab on:connect/>
|
||||
{:else if activeInfo === 'CPU'}
|
||||
<CpuTab/>
|
||||
{:else if activeInfo === 'Disk'}
|
||||
<DiskTab on:reset/>
|
||||
{:else if activeInfo === 'Posts'}
|
||||
<PostsTab/>
|
||||
{:else if activeInfo === 'Discord'}
|
||||
<DiscordTab/>
|
||||
{:else if activeInfo === 'GitHub'}
|
||||
<GitHubTab/>
|
||||
{:else}
|
||||
<p>TODO: {activeInfo}</p>
|
||||
{/if}
|
||||
<div class="mt-auto text-sm text-gray-300">
|
||||
<div class="pt-1 pb-1">
|
||||
<a href="https://cheerpx.io/" target="_blank">
|
||||
<span>Powered by CheerpX</span>
|
||||
<img src="assets/cheerpx.svg" alt="CheerpX Logo" class="w-6 h-6 inline-block">
|
||||
</a>
|
||||
</div>
|
||||
<hr class="border-t border-solid border-gray-300">
|
||||
<div class="pt-1 pb-1">
|
||||
<a href="https://leaningtech.com/" target="”_blank”">© 2022-2024 Leaning Technologies</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
341
src/lib/WebVM.svelte
Normal file
|
@ -0,0 +1,341 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import Nav from 'labs/packages/global-navbar/src/Nav.svelte';
|
||||
import SideBar from '$lib/SideBar.svelte';
|
||||
import '$lib/global.css';
|
||||
import '@xterm/xterm/css/xterm.css'
|
||||
import '@fortawesome/fontawesome-free/css/all.min.css'
|
||||
import { networkInterface, startLogin } from '$lib/network.js'
|
||||
import { cpuActivity, diskActivity, cpuPercentage, diskLatency } from '$lib/activities.js'
|
||||
import { introMessage, errorMessage, unexpectedErrorMessage } from '$lib/messages.js'
|
||||
|
||||
export let configObj = null;
|
||||
export let processCallback = null;
|
||||
export let cacheId = null;
|
||||
export let cpuActivityEvents = [];
|
||||
export let diskLatencies = [];
|
||||
export let activityEventsInterval = 0;
|
||||
|
||||
var term = null;
|
||||
var cx = null;
|
||||
var fitAddon = null;
|
||||
var cxReadFunc = null;
|
||||
var blockCache = null;
|
||||
var processCount = 0;
|
||||
var curVT = 0;
|
||||
function writeData(buf, vt)
|
||||
{
|
||||
if(vt != 1)
|
||||
return;
|
||||
term.write(new Uint8Array(buf));
|
||||
}
|
||||
function readData(str)
|
||||
{
|
||||
if(cxReadFunc == null)
|
||||
return;
|
||||
for(var i=0;i<str.length;i++)
|
||||
cxReadFunc(str.charCodeAt(i));
|
||||
}
|
||||
function printMessage(msg)
|
||||
{
|
||||
for(var i=0;i<msg.length;i++)
|
||||
term.write(msg[i] + "\n");
|
||||
}
|
||||
function expireEvents(list, curTime, limitTime)
|
||||
{
|
||||
while(list.length > 1)
|
||||
{
|
||||
if(list[1].t < limitTime)
|
||||
{
|
||||
list.shift();
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
function cleanupEvents()
|
||||
{
|
||||
var curTime = Date.now();
|
||||
var limitTime = curTime - 10000;
|
||||
expireEvents(cpuActivityEvents, curTime, limitTime);
|
||||
computeCpuActivity(curTime, limitTime);
|
||||
if(cpuActivityEvents.length == 0)
|
||||
{
|
||||
clearInterval(activityEventsInterval);
|
||||
activityEventsInterval = 0;
|
||||
}
|
||||
}
|
||||
function computeCpuActivity(curTime, limitTime)
|
||||
{
|
||||
var totalActiveTime = 0;
|
||||
var lastActiveTime = limitTime;
|
||||
var lastWasActive = false;
|
||||
for(var i=0;i<cpuActivityEvents.length;i++)
|
||||
{
|
||||
var e = cpuActivityEvents[i];
|
||||
// NOTE: The first event could be before the limit,
|
||||
// we need at least one event to correctly mark
|
||||
// active time when there is long time under load
|
||||
var eTime = e.t;
|
||||
if(eTime < limitTime)
|
||||
eTime = limitTime;
|
||||
if(e.state == "ready")
|
||||
{
|
||||
// Inactive state, add the time frome lastActiveTime
|
||||
totalActiveTime += (eTime - lastActiveTime);
|
||||
lastWasActive = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Active state
|
||||
lastActiveTime = eTime;
|
||||
lastWasActive = true;
|
||||
}
|
||||
}
|
||||
// Add the last interval if needed
|
||||
if(lastWasActive)
|
||||
{
|
||||
totalActiveTime += (curTime - lastActiveTime);
|
||||
}
|
||||
cpuPercentage.set(Math.ceil((totalActiveTime / 10000) * 100));
|
||||
}
|
||||
function hddCallback(state)
|
||||
{
|
||||
diskActivity.set(state != "ready");
|
||||
}
|
||||
function latencyCallback(latency)
|
||||
{
|
||||
diskLatencies.push(latency);
|
||||
if(diskLatencies.length > 30)
|
||||
diskLatencies.shift();
|
||||
// Average the latency over at most 30 blocks
|
||||
var total = 0;
|
||||
for(var i=0;i<diskLatencies.length;i++)
|
||||
total += diskLatencies[i];
|
||||
var avg = total / diskLatencies.length;
|
||||
diskLatency.set(Math.ceil(avg));
|
||||
}
|
||||
function cpuCallback(state)
|
||||
{
|
||||
cpuActivity.set(state != "ready");
|
||||
var curTime = Date.now();
|
||||
var limitTime = curTime - 10000;
|
||||
expireEvents(cpuActivityEvents, curTime, limitTime);
|
||||
cpuActivityEvents.push({t: curTime, state: state});
|
||||
computeCpuActivity(curTime, limitTime);
|
||||
// Start an interval timer to cleanup old samples when no further activity is received
|
||||
if(activityEventsInterval != 0)
|
||||
clearInterval(activityEventsInterval);
|
||||
activityEventsInterval = setInterval(cleanupEvents, 2000);
|
||||
}
|
||||
function computeXTermFontSize()
|
||||
{
|
||||
return parseInt(getComputedStyle(document.body).fontSize);
|
||||
}
|
||||
function setScreenSize(display)
|
||||
{
|
||||
var mult = 1.0;
|
||||
var displayWidth = display.offsetWidth;
|
||||
var displayHeight = display.offsetHeight;
|
||||
var minWidth = 1024;
|
||||
var minHeight = 768;
|
||||
if(displayWidth < minWidth)
|
||||
mult = minWidth / displayWidth;
|
||||
if(displayHeight < minHeight)
|
||||
mult = Math.max(mult, minHeight / displayHeight);
|
||||
cx.setKmsCanvas(display, displayWidth * mult, displayHeight * mult);
|
||||
}
|
||||
var curInnerWidth = 0;
|
||||
var curInnerHeight = 0;
|
||||
function handleResize()
|
||||
{
|
||||
// Avoid spurious resize events caused by the soft keyboard
|
||||
if(curInnerWidth == window.innerWidth && curInnerHeight == window.innerHeight)
|
||||
return;
|
||||
curInnerWidth = window.innerWidth;
|
||||
curInnerHeight = window.innerHeight;
|
||||
term.options.fontSize = computeXTermFontSize();
|
||||
fitAddon.fit();
|
||||
const display = document.getElementById("display");
|
||||
if(display)
|
||||
setScreenSize(display);
|
||||
}
|
||||
async function initTerminal()
|
||||
{
|
||||
const { Terminal } = await import('@xterm/xterm');
|
||||
const { FitAddon } = await import('@xterm/addon-fit');
|
||||
const { WebLinksAddon } = await import('@xterm/addon-web-links');
|
||||
term = new Terminal({cursorBlink:true, convertEol:true, fontFamily:"monospace", fontWeight: 400, fontWeightBold: 700, fontSize: computeXTermFontSize()});
|
||||
fitAddon = new FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
var linkAddon = new WebLinksAddon();
|
||||
term.loadAddon(linkAddon);
|
||||
const consoleDiv = document.getElementById("console");
|
||||
term.open(consoleDiv);
|
||||
term.scrollToTop();
|
||||
fitAddon.fit();
|
||||
window.addEventListener("resize", handleResize);
|
||||
term.focus();
|
||||
term.onData(readData);
|
||||
// Avoid undesired default DnD handling
|
||||
function preventDefaults (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
consoleDiv.addEventListener("dragover", preventDefaults, false);
|
||||
consoleDiv.addEventListener("dragenter", preventDefaults, false);
|
||||
consoleDiv.addEventListener("dragleave", preventDefaults, false);
|
||||
consoleDiv.addEventListener("drop", preventDefaults, false);
|
||||
curInnerWidth = window.innerWidth;
|
||||
curInnerHeight = window.innerHeight;
|
||||
if(configObj.printIntro)
|
||||
printMessage(introMessage);
|
||||
try
|
||||
{
|
||||
await initCheerpX();
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
printMessage(unexpectedErrorMessage);
|
||||
printMessage([e.toString()]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
function handleActivateConsole(vt)
|
||||
{
|
||||
if(curVT == vt)
|
||||
return;
|
||||
curVT = vt;
|
||||
if(vt != 7)
|
||||
return;
|
||||
// Raise the display to the foreground
|
||||
const display = document.getElementById("display");
|
||||
display.parentElement.style.zIndex = 5;
|
||||
plausible("Display activated");
|
||||
}
|
||||
function handleProcessCreated()
|
||||
{
|
||||
processCount++;
|
||||
if(processCallback)
|
||||
processCallback(processCount);
|
||||
}
|
||||
async function initCheerpX()
|
||||
{
|
||||
const CheerpX = await import('@leaningtech/cheerpx');
|
||||
var blockDevice = null;
|
||||
switch(configObj.diskImageType)
|
||||
{
|
||||
case "cloud":
|
||||
try
|
||||
{
|
||||
blockDevice = await CheerpX.CloudDevice.create(configObj.diskImageUrl);
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
// Report the failure and try again with plain HTTP
|
||||
var wssProtocol = "wss:";
|
||||
if(configObj.diskImageUrl.startsWith(wssProtocol))
|
||||
{
|
||||
// WebSocket protocol failed, try agin using plain HTTP
|
||||
plausible("WS Disk failure");
|
||||
blockDevice = await CheerpX.CloudDevice.create("https:" + configObj.diskImageUrl.substr(wssProtocol.length));
|
||||
}
|
||||
else
|
||||
{
|
||||
// No other recovery option
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "bytes":
|
||||
blockDevice = await CheerpX.HttpBytesDevice.create(configObj.diskImageUrl);
|
||||
break;
|
||||
case "github":
|
||||
blockDevice = await CheerpX.GitHubDevice.create(configObj.diskImageUrl);
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unrecognized device type");
|
||||
}
|
||||
blockCache = await CheerpX.IDBDevice.create(cacheId);
|
||||
var overlayDevice = await CheerpX.OverlayDevice.create(blockDevice, blockCache);
|
||||
var webDevice = await CheerpX.WebDevice.create("");
|
||||
var documentsDevice = await CheerpX.WebDevice.create("documents");
|
||||
var dataDevice = await CheerpX.DataDevice.create();
|
||||
var mountPoints = [
|
||||
// The root filesystem, as an Ext2 image
|
||||
{type:"ext2", dev:overlayDevice, path:"/"},
|
||||
// Access to files on the Web server, relative to the current page
|
||||
{type:"dir", dev:webDevice, path:"/web"},
|
||||
// Access to read-only data coming from JavaScript
|
||||
{type:"dir", dev:dataDevice, path:"/data"},
|
||||
// Automatically created device files
|
||||
{type:"devs", path:"/dev"},
|
||||
// Pseudo-terminals
|
||||
{type:"devpts", path:"/dev/pts"},
|
||||
// The Linux 'proc' filesystem which provides information about running processes
|
||||
{type:"proc", path:"/proc"},
|
||||
// Convenient access to sample documents in the user directory
|
||||
{type:"dir", dev:documentsDevice, path:"/home/user/documents"}
|
||||
];
|
||||
try
|
||||
{
|
||||
cx = await CheerpX.Linux.create({mounts: mountPoints, networkInterface: networkInterface});
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
printMessage(errorMessage);
|
||||
printMessage([e.toString()]);
|
||||
return;
|
||||
}
|
||||
cx.registerCallback("cpuActivity", cpuCallback);
|
||||
cx.registerCallback("diskActivity", hddCallback);
|
||||
cx.registerCallback("diskLatency", latencyCallback);
|
||||
cx.registerCallback("processCreated", handleProcessCreated);
|
||||
term.scrollToBottom();
|
||||
cxReadFunc = cx.setCustomConsole(writeData, term.cols, term.rows);
|
||||
const display = document.getElementById("display");
|
||||
if(display)
|
||||
{
|
||||
setScreenSize(display);
|
||||
cx.setActivateConsole(handleActivateConsole);
|
||||
}
|
||||
// Run the command in a loop, in case the user exits
|
||||
while (true)
|
||||
{
|
||||
await cx.run(configObj.cmd, configObj.args, configObj.opts);
|
||||
}
|
||||
}
|
||||
onMount(initTerminal);
|
||||
async function handleConnect()
|
||||
{
|
||||
const w = window.open("login.html", "_blank");
|
||||
await cx.networkLogin();
|
||||
w.location.href = await startLogin();
|
||||
}
|
||||
async function handleReset()
|
||||
{
|
||||
// Be robust before initialization
|
||||
if(blockCache == null)
|
||||
return;
|
||||
await blockCache.reset();
|
||||
location.reload();
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="relative w-full h-full">
|
||||
<Nav />
|
||||
<div class="absolute top-10 bottom-0 left-0 right-0">
|
||||
<SideBar on:connect={handleConnect} on:reset={handleReset}>
|
||||
<slot></slot>
|
||||
</SideBar>
|
||||
{#if configObj.needsDisplay}
|
||||
<div class="absolute top-0 bottom-0 left-14 right-0">
|
||||
<canvas class="w-full h-full cursor-none" id="display"></canvas>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="absolute top-0 bottom-0 left-14 right-0 p-1 scrollbar" id="console">
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
6
src/lib/activities.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { writable } from 'svelte/store';
|
||||
|
||||
export const cpuActivity = writable(false);
|
||||
export const diskActivity = writable(false);
|
||||
export const cpuPercentage = writable(0);
|
||||
export const diskLatency = writable(0);
|
26
src/lib/global.css
Normal file
|
@ -0,0 +1,26 @@
|
|||
@import url('https://fonts.googleapis.com/css2?family=Archivo:ital,wght@0,100..900;1,100..900&display=swap');
|
||||
|
||||
@tailwind base;
|
||||
@tailwind utilities;
|
||||
|
||||
body
|
||||
{
|
||||
font-family: Archivo, sans-serif;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: black;
|
||||
}
|
||||
|
||||
html
|
||||
{
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media (width <= 850px)
|
||||
{
|
||||
html
|
||||
{
|
||||
font-size: calc(100vw / 55);
|
||||
}
|
||||
}
|
51
src/lib/messages.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
const color= "\x1b[1;35m";
|
||||
const underline= "\x1b[94;4m";
|
||||
const normal= "\x1b[0m";
|
||||
export const introMessage = [
|
||||
"+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+",
|
||||
"| |",
|
||||
"| WebVM is a virtual Linux environment running in the browser via WebAssembly |",
|
||||
"| |",
|
||||
"| WebVM is powered by the CheerpX virtualization engine, which enables safe, |",
|
||||
"| sandboxed client-side execution of x86 binaries, fully client-side |",
|
||||
"| |",
|
||||
"| CheerpX includes an x86-to-WebAssembly JIT compiler, a virtual block-based |",
|
||||
"| file system, and a Linux syscall emulator |",
|
||||
"| |",
|
||||
"| [News] WebVM 2.0: A complete Linux Desktop Environment in the browser: |",
|
||||
"| |",
|
||||
"| " + underline + "https://labs.leaningtech.com/blog/webvm-20" + normal + " |",
|
||||
"| |",
|
||||
"| Try out the new Alpine / Xorg / i3 WebVM: " + underline + "https://webvm.io/alpine.html" + normal + " |",
|
||||
"| |",
|
||||
"+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+",
|
||||
"",
|
||||
" Welcome to WebVM. If unsure, try these examples:",
|
||||
"",
|
||||
" python3 examples/python3/fibonacci.py ",
|
||||
" gcc -o helloworld examples/c/helloworld.c && ./helloworld",
|
||||
" objdump -d ./helloworld | less -M",
|
||||
" vim examples/c/helloworld.c",
|
||||
" curl --max-time 15 parrot.live # requires networking",
|
||||
""
|
||||
];
|
||||
export const errorMessage = [
|
||||
color + "CheerpX could not start" + normal,
|
||||
"",
|
||||
"CheerpX is expected to work with recent desktop versions of Chrome, Edge, Firefox and Safari",
|
||||
"",
|
||||
"Give it a try from a desktop version / another browser!",
|
||||
"",
|
||||
"CheerpX internal error message is:",
|
||||
""
|
||||
];
|
||||
export const unexpectedErrorMessage = [
|
||||
color + "WebVM encountered an unexpected error" + normal,
|
||||
"",
|
||||
"Check the DevTools console for further information",
|
||||
"",
|
||||
"Please consider reporting a bug!",
|
||||
"",
|
||||
"CheerpX internal error message is:",
|
||||
""
|
||||
];
|
66
src/lib/network.js
Normal file
|
@ -0,0 +1,66 @@
|
|||
import { writable } from 'svelte/store';
|
||||
import { browser } from '$app/environment'
|
||||
|
||||
let authKey = undefined;
|
||||
let controlUrl = undefined;
|
||||
if(browser)
|
||||
{
|
||||
let params = new URLSearchParams("?"+window.location.hash.substr(1));
|
||||
authKey = params.get("authKey") || undefined;
|
||||
controlUrl = params.get("controlUrl") || undefined;
|
||||
}
|
||||
let dashboardUrl = controlUrl ? null : "https://login.tailscale.com/admin/machines";
|
||||
let resolveLogin = null;
|
||||
let loginPromise = new Promise((f,r) => {
|
||||
resolveLogin = f;
|
||||
});
|
||||
let connectionState = writable("DISCONNECTED");
|
||||
let exitNode = writable(false);
|
||||
|
||||
function loginUrlCb(url)
|
||||
{
|
||||
connectionState.set("LOGINREADY");
|
||||
resolveLogin(url);
|
||||
}
|
||||
|
||||
function stateUpdateCb(state)
|
||||
{
|
||||
switch(state)
|
||||
{
|
||||
case 6 /*Running*/:
|
||||
{
|
||||
connectionState.set("CONNECTED");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function netmapUpdateCb(map)
|
||||
{
|
||||
networkData.currentIp = map.self.addresses[0];
|
||||
var exitNodeFound = false;
|
||||
for(var i=0;map.peers.length;i++)
|
||||
{
|
||||
if(map.peers[i].exitNode)
|
||||
{
|
||||
exitNodeFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(exitNodeFound)
|
||||
{
|
||||
exitNode.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
export async function startLogin()
|
||||
{
|
||||
connectionState.set("LOGINSTARTING");
|
||||
const url = await loginPromise;
|
||||
networkData.loginUrl = url;
|
||||
return url;
|
||||
}
|
||||
|
||||
export const networkInterface = { authKey: authKey, controlUrl: controlUrl, loginUrlCb: loginUrlCb, stateUpdateCb: stateUpdateCb, netmapUpdateCb: netmapUpdateCb };
|
||||
|
||||
export const networkData = { currentIp: null, connectionState: connectionState, exitNode: exitNode, loginUrl: null, dashboardUrl: dashboardUrl }
|
44
src/routes/+layout.server.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { parse } from 'node-html-parser';
|
||||
import { read } from '$app/server';
|
||||
|
||||
var posts = [
|
||||
"https://labs.leaningtech.com/blog/webvm-20",
|
||||
"https://labs.leaningtech.com/blog/join-the-webvm-hackathon",
|
||||
"https://labs.leaningtech.com/blog/mini-webvm-your-linux-box-from-dockerfile-via-wasm",
|
||||
"https://labs.leaningtech.com/blog/webvm-virtual-machine-with-networking-via-tailscale",
|
||||
"https://labs.leaningtech.com/blog/webvm-server-less-x86-virtual-machines-in-the-browser",
|
||||
];
|
||||
|
||||
async function getPostData(u)
|
||||
{
|
||||
var ret = { title: null, image: null, url: u };
|
||||
var response = await fetch(u);
|
||||
var str = await response.text();
|
||||
var root = parse(str);
|
||||
var tags = root.getElementsByTagName("meta");
|
||||
for(var i=0;i<tags.length;i++)
|
||||
{
|
||||
var metaName = tags[i].getAttribute("property");
|
||||
var metaContent = tags[i].getAttribute("content");
|
||||
switch(metaName)
|
||||
{
|
||||
case "og:title":
|
||||
ret.title = metaContent;
|
||||
break;
|
||||
case "og:image":
|
||||
ret.image = metaContent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
export async function load()
|
||||
{
|
||||
var ret = [];
|
||||
for(var i=0;i<posts.length;i++)
|
||||
{
|
||||
ret.push(await getPostData(posts[i]));
|
||||
}
|
||||
return { posts: ret };
|
||||
}
|
1
src/routes/+page.js
Normal file
|
@ -0,0 +1 @@
|
|||
export const prerender = true;
|
16
src/routes/+page.svelte
Normal file
|
@ -0,0 +1,16 @@
|
|||
<script>
|
||||
import WebVM from '$lib/WebVM.svelte';
|
||||
import * as configObj from '/config_terminal'
|
||||
function handleProcessCreated(processCount)
|
||||
{
|
||||
// Log the first 5 processes, to get an idea of the level of interaction from the public
|
||||
if(processCount <= 5)
|
||||
{
|
||||
plausible(`Process started: ${processCount}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<WebVM configObj={configObj} processCallback={handleProcessCreated} cacheId="blocks_terminal">
|
||||
<p>Looking for a complete desktop experience? Try the new <a class="underline" href="/alpine.html" target="_blank">Alpine Linux</a> graphical WebVM</p>
|
||||
</WebVM>
|
1
src/routes/alpine/+page.js
Normal file
|
@ -0,0 +1 @@
|
|||
export const prerender = true;
|
16
src/routes/alpine/+page.svelte
Normal file
|
@ -0,0 +1,16 @@
|
|||
<script>
|
||||
import WebVM from '$lib/WebVM.svelte';
|
||||
import * as configObj from '/config_public_alpine'
|
||||
function handleProcessCreated(processCount)
|
||||
{
|
||||
// Log only the first process, as a proxy for successful startup
|
||||
if(processCount == 1)
|
||||
{
|
||||
plausible("Alpine init");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<WebVM configObj={configObj} processCallback={handleProcessCreated} cacheId="blocks_alpine">
|
||||
<p>Looking for something different? Try the classic <a class="underline" href="/" target="_blank">Debian Linux</a> terminal-based WebVM</p>
|
||||
</WebVM>
|
12
svelte.config.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import adapter from '@sveltejs/adapter-static';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
kit: {
|
||||
adapter: adapter()
|
||||
},
|
||||
preprocess: vitePreprocess()
|
||||
};
|
||||
|
||||
export default config;
|
9
tailwind.config.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
BIN
tower.ico
Normal file
After Width: | Height: | Size: 73 KiB |
28
vite.config.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import { viteStaticCopy } from 'vite-plugin-static-copy';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'/config_terminal': process.env.WEBVM_MODE == "github" ? 'config_github_terminal.js' : 'config_public_terminal.js',
|
||||
"@leaningtech/cheerpx": process.env.CX_URL ? process.env.CX_URL : "@leaningtech/cheerpx"
|
||||
}
|
||||
},
|
||||
build: {
|
||||
target: "es2022"
|
||||
},
|
||||
plugins: [
|
||||
sveltekit(),
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{ src: 'tower.ico', dest: '' },
|
||||
{ src: 'scrollbar.css', dest: '' },
|
||||
{ src: 'serviceWorker.js', dest: '' },
|
||||
{ src: 'login.html', dest: '' },
|
||||
{ src: 'assets/', dest: '' },
|
||||
{ src: 'documents/', dest: '' }
|
||||
]
|
||||
})
|
||||
]
|
||||
});
|
|
@ -1,2 +1,2 @@
|
|||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(self,(function(){return(()=>{"use strict";var e={775:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0;var r=function(){function e(){}return e.prototype.activate=function(e){this._terminal=e},e.prototype.dispose=function(){},e.prototype.fit=function(){var e=this.proposeDimensions();if(e&&this._terminal){var t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}},e.prototype.proposeDimensions=function(){if(this._terminal&&this._terminal.element&&this._terminal.element.parentElement){var e=this._terminal._core;if(0!==e._renderService.dimensions.actualCellWidth&&0!==e._renderService.dimensions.actualCellHeight){var t=window.getComputedStyle(this._terminal.element.parentElement),r=parseInt(t.getPropertyValue("height")),i=Math.max(0,parseInt(t.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),o=r-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=i-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-e.viewport.scrollBarWidth;return{cols:Math.max(2,Math.floor(a/e._renderService.dimensions.actualCellWidth)),rows:Math.max(1,Math.floor(o/e._renderService.dimensions.actualCellHeight))}}}},e}();t.FitAddon=r}},t={};return function r(i){if(t[i])return t[i].exports;var n=t[i]={exports:{}};return e[i](n,n.exports,r),n.exports}(775)})()}));
|
||||
//# sourceMappingURL=xterm-addon-fit.js.map
|
||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(self,(()=>(()=>{"use strict";var e={};return(()=>{var t=e;Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0,t.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,i=window.getComputedStyle(this._terminal.element.parentElement),o=parseInt(i.getPropertyValue("height")),s=Math.max(0,parseInt(i.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),l=o-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=s-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}}})(),e})()));
|
||||
//# sourceMappingURL=addon-fit.js.map
|
||||
|
|
2
xterm/xterm-addon-web-links.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.WebLinksAddon=t():e.WebLinksAddon=t()}(self,(()=>(()=>{"use strict";var e={6:(e,t)=>{function n(e){try{const t=new URL(e),n=t.password&&t.username?`${t.protocol}//${t.username}:${t.password}@${t.host}`:t.username?`${t.protocol}//${t.username}@${t.host}`:`${t.protocol}//${t.host}`;return e.toLocaleLowerCase().startsWith(n.toLocaleLowerCase())}catch(e){return!1}}Object.defineProperty(t,"__esModule",{value:!0}),t.LinkComputer=t.WebLinkProvider=void 0,t.WebLinkProvider=class{constructor(e,t,n,o={}){this._terminal=e,this._regex=t,this._handler=n,this._options=o}provideLinks(e,t){const n=o.computeLink(e,this._regex,this._terminal,this._handler);t(this._addCallbacks(n))}_addCallbacks(e){return e.map((e=>(e.leave=this._options.leave,e.hover=(t,n)=>{if(this._options.hover){const{range:o}=e;this._options.hover(t,n,o)}},e)))}};class o{static computeLink(e,t,r,i){const s=new RegExp(t.source,(t.flags||"")+"g"),[a,c]=o._getWindowedLineStrings(e-1,r),l=a.join("");let d;const p=[];for(;d=s.exec(l);){const e=d[0];if(!n(e))continue;const[t,s]=o._mapStrIdx(r,c,0,d.index),[a,l]=o._mapStrIdx(r,t,s,e.length);if(-1===t||-1===s||-1===a||-1===l)continue;const h={start:{x:s+1,y:t+1},end:{x:l,y:a+1}};p.push({range:h,text:e,activate:i})}return p}static _getWindowedLineStrings(e,t){let n,o=e,r=e,i=0,s="";const a=[];if(n=t.buffer.active.getLine(e)){const e=n.translateToString(!0);if(n.isWrapped&&" "!==e[0]){for(i=0;(n=t.buffer.active.getLine(--o))&&i<2048&&(s=n.translateToString(!0),i+=s.length,a.push(s),n.isWrapped&&-1===s.indexOf(" ")););a.reverse()}for(a.push(e),i=0;(n=t.buffer.active.getLine(++r))&&n.isWrapped&&i<2048&&(s=n.translateToString(!0),i+=s.length,a.push(s),-1===s.indexOf(" ")););}return[a,o]}static _mapStrIdx(e,t,n,o){const r=e.buffer.active,i=r.getNullCell();let s=n;for(;o;){const e=r.getLine(t);if(!e)return[-1,-1];for(let n=s;n<e.length;++n){e.getCell(n,i);const s=i.getChars();if(i.getWidth()&&(o-=s.length||1,n===e.length-1&&""===s)){const e=r.getLine(t+1);e&&e.isWrapped&&(e.getCell(0,i),2===i.getWidth()&&(o+=1))}if(o<0)return[t,n]}t++,s=0}return[t,s]}}t.LinkComputer=o}},t={};function n(o){var r=t[o];if(void 0!==r)return r.exports;var i=t[o]={exports:{}};return e[o](i,i.exports,n),i.exports}var o={};return(()=>{var e=o;Object.defineProperty(e,"__esModule",{value:!0}),e.WebLinksAddon=void 0;const t=n(6),r=/(https?|HTTPS?):[/]{2}[^\s"'!*(){}|\\\^<>`]*[^\s"':,.!?{}|\\\^~\[\]`()<>]/;function i(e,t){const n=window.open();if(n){try{n.opener=null}catch{}n.location.href=t}else console.warn("Opening link blocked as opener could not be cleared")}e.WebLinksAddon=class{constructor(e=i,t={}){this._handler=e,this._options=t}activate(e){this._terminal=e;const n=this._options,o=n.urlRegex||r;this._linkProvider=this._terminal.registerLinkProvider(new t.WebLinkProvider(this._terminal,o,this._handler,n))}dispose(){this._linkProvider?.dispose()}}})(),o})()));
|
||||
//# sourceMappingURL=addon-web-links.js.map
|
|
@ -36,6 +36,7 @@
|
|||
*/
|
||||
|
||||
.xterm {
|
||||
cursor: text;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
|
@ -124,10 +125,6 @@
|
|||
line-height: normal;
|
||||
}
|
||||
|
||||
.xterm {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.xterm.enable-mouse-events {
|
||||
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
|
||||
cursor: default;
|
||||
|
@ -143,7 +140,7 @@
|
|||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.xterm .xterm-accessibility,
|
||||
.xterm .xterm-accessibility:not(.debug),
|
||||
.xterm .xterm-message {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
@ -152,6 +149,16 @@
|
|||
right: 0;
|
||||
z-index: 10;
|
||||
color: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.xterm .xterm-accessibility-tree:not(.debug) *::selection {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.xterm .xterm-accessibility-tree {
|
||||
user-select: text;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.xterm .live-region {
|
||||
|
@ -163,13 +170,49 @@
|
|||
}
|
||||
|
||||
.xterm-dim {
|
||||
opacity: 0.5;
|
||||
/* Dim should not apply to background, so the opacity of the foreground color is applied
|
||||
* explicitly in the generated class and reset to 1 here */
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.xterm-underline {
|
||||
text-decoration: underline;
|
||||
.xterm-underline-1 { text-decoration: underline; }
|
||||
.xterm-underline-2 { text-decoration: double underline; }
|
||||
.xterm-underline-3 { text-decoration: wavy underline; }
|
||||
.xterm-underline-4 { text-decoration: dotted underline; }
|
||||
.xterm-underline-5 { text-decoration: dashed underline; }
|
||||
|
||||
.xterm-overline {
|
||||
text-decoration: overline;
|
||||
}
|
||||
|
||||
.xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
|
||||
.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }
|
||||
.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }
|
||||
.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; }
|
||||
.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }
|
||||
|
||||
.xterm-strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.xterm-screen .xterm-decoration-container .xterm-decoration {
|
||||
z-index: 6;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
|
||||
z-index: 7;
|
||||
}
|
||||
|
||||
.xterm-decoration-overview-ruler {
|
||||
z-index: 8;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.xterm-decoration-top {
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
}
|
||||
|
|