Compare commits
267 commits
servicewor
...
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 |
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,70 +1,118 @@
|
|||
# WebVM
|
||||
|
||||
This repository hosts the source code of for [https://webvm.io](https://webvm.io), a Linux virtual machine that runs in your browser.
|
||||
[](https://discord.gg/yWRr2YnD9c)
|
||||
[](https://github.com/leaningtech/webvm/issues)
|
||||
|
||||
<img src="assets/welcome_to_WebVM_slim.png" width="70%">
|
||||
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.
|
||||
|
||||
# How to: general usage
|
||||
# Enable networking
|
||||
|
||||
- go to [https://webvm.io](https://webvm.io)
|
||||
- use the provided terminal environment
|
||||
- have fun!
|
||||
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.
|
||||
|
||||
# How to: enable networking
|
||||
- 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).
|
||||
|
||||
- go to [https://webvm.io](https://webvm.io)
|
||||
- click "Tailscale Login" in the top right corner
|
||||
- log in to Tailscale (create an accout if you don't have one)
|
||||
- if you want to access the public internet, you need an Exit Node. See [here](https://tailscale.com/kb/1103/exit-nodes/) for how to set one up. If you just want to access a machine in your Tailscale Network, you don't need it
|
||||
- depending on your network speed, you may need to wait a few moments for the Tailscale Wasm module to be downloaded
|
||||
- log in with your Tailscale credentials
|
||||
- go back to the WebVM tab. You will see your IP address in the top right
|
||||
- start firing network requests!
|
||||
# Fork, deploy, customize
|
||||
|
||||
# How to: login to Tailscale with an Auth key
|
||||
<img src="/assets/fork_deploy_instructions.gif" alt="deploy_instructions_gif" width="90%">
|
||||
|
||||
- Add `#authKey=<your-key>` at the end of the URL
|
||||
- Done. You don't need to manually log in anymore
|
||||
- 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.
|
||||
|
||||
It is recommended to use an ephemeral key.
|
||||
<img src="/assets/result.png" width="70%" >
|
||||
|
||||
# How to: login to a self-hosted Tailscale network (Headscale)
|
||||
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.
|
||||
|
||||
- Add `#controlUrl=<your-control-url>` at the end of the URL
|
||||
- You can combine this option with `authKey` with a `&`: `#controlUrl=<url>&authKey=<key>`
|
||||
# Local deployment
|
||||
|
||||
# How to host WebVM locally
|
||||
From a local `git clone`
|
||||
|
||||
- Replace `CX_VERSION` in index.html and tinycore.html with a valid version of CheerpX. The latest version can be found at [https://webvm.io](https://webvm.io)
|
||||
- Run nginx -p . -c nginx.conf in the root of the WebVM directory. WebVM can then be found at `http://localhost:8081`
|
||||
- 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](https://github.com/leaningtech/webvm/issues) to report any bug.
|
||||
Or come to say hello / share your feedback on [Discord](https://discord.leaningtech.com).
|
||||
|
||||
# Browsers support
|
||||
|
||||
WebVM and CheerpX are compatible with any browser, both Desktop and Mobile, provided support for [SAB](https://caniuse.com/sharedarraybuffer), [IndexedDB](https://caniuse.com/indexeddb), and the device having enough memory.
|
||||
Or come to say hello / share your feedback on [Discord](https://discord.gg/yTNZgySKGa).
|
||||
|
||||
# More links
|
||||
|
||||
- [Do: WebVM](https://webvm.io)
|
||||
- [Read: WebVM](https://leaningtech.com/webvm-server-less-x86-virtual-machines-in-the-browser/)
|
||||
- [Read: WebVM + Tailscale networking](https://leaningtech.com/webvm-virtual-machine-with-networking-via-tailscale/)
|
||||
- [Learn: WebVM](https://leaningtech.com/webvm)
|
||||
- [Watch: WebVM at GitNation](https://www.youtube.com/watch?v=VqrbVycTXmw)
|
||||
- [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
|
||||
|
||||
# Thanks to...
|
||||
This project depends on:
|
||||
- CheerpX, made by [Leaning Technologies](https://leaningtech.com) for the virtualization part
|
||||
- [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 to the Web by [Cheerp](https://github.com/leaningtech/cheerp-meta)
|
||||
- [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/)
|
||||
|
||||
# Versioning
|
||||
|
||||
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).
|
||||
|
||||
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
|
||||
|
|
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/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 |
BIN
assets/webvm_hero.png
Normal file
After Width: | Height: | Size: 514 KiB |
Before Width: | Height: | Size: 70 KiB |
BIN
assets/welcome_to_WebVM_2024.png
Normal file
After Width: | Height: | Size: 248 KiB |
Before Width: | Height: | Size: 77 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 }
|
||||
|
||||
|
|
301
index.html
|
@ -1,301 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<!-- Serviceworker script that adds the COI and CORS headers to the response headers in cases where the server does not support it. -->
|
||||
<script src="serviceWorker.js"></script>
|
||||
|
||||
<html lang="en" style="height:100%;">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>WebVM - Linux virtualization in WebAssembly</title>
|
||||
|
||||
<meta name="description" content="Server-less virtual machine, networking included, running browser-side in HTML5/WebAssembly. Code in any programming language inside this Linux terminal.">
|
||||
<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:url" content="/">
|
||||
<meta property="og:image" content="https://webvm.io/assets/welcome_to_WebVM_.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="Server-less virtual machine, networking included, running browser-side in HTML5/WebAssembly. Code in any programming language inside this Linux terminal.">
|
||||
<meta name="twitter:image" content="https://webvm.io/assets/welcome_to_WebVM_.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" id="us-fonts-css" href="https://fonts.googleapis.com/css?family=Montserrat%3A300%2C400%2C500%2C600%2C700&display=swap&ver=6.0.2" media="all">
|
||||
<link rel="stylesheet" href="./xterm/xterm.css" />
|
||||
<link rel="stylesheet" href="./scrollbar.css" />
|
||||
<script src="./xterm/xterm.js"></script>
|
||||
<script src="./xterm/xterm-addon-fit.js"></script>
|
||||
<script>
|
||||
window.networkInterface = { ready: false };
|
||||
</script>
|
||||
<script type="module" src="network.js"></script>
|
||||
</head>
|
||||
|
||||
<body style="margin:0;height:100%;background:black;color:white;overflow:hidden; display:flex; flex-direction: column; justify-content: space-between; height: 100%;">
|
||||
|
||||
<header style="flex-grow:0; flex-srink: 0;height:80px; width: 100%; margin: 2px 0 2px 0;">
|
||||
<div style="display: flex; flex-direction: row; justify-content: space-between; width: 100%;">
|
||||
<div style="display: flex; flex-direction: row;">
|
||||
<a href="https://leaningtech.com/" target="_blank">
|
||||
<img src="./assets/leaningtech.png" style="margin-left: 10px; height: 60px; margin-top: 10px;">
|
||||
</a>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: row; justify-content: space-before;">
|
||||
<li style=" margin-right: 50px; height: 100%; display: flex; align-items: center;">
|
||||
<a href="https://discord.leaningtech.com" target="_blank" style="text-decoration: none">
|
||||
<div style="color: white; font-family: montserrat; font-weight: 700; font-size: large;">Discord</div>
|
||||
</a>
|
||||
</li>
|
||||
<li style=" margin-right: 50px; height: 100%; display: flex; align-items: center;">
|
||||
<a href="https://github.com/leaningtech/webvm" target="_blank" style="text-decoration: none" >
|
||||
<div style="color: white; font-family: montserrat; font-weight: 700; font-size: large;">Github</div>
|
||||
</a>
|
||||
</li>
|
||||
<li style=" margin-right: 50px; height: 100%; display: flex; align-items: center;">
|
||||
<a id="loginLink" style="text-decoration: none; cursor:not-allowed;">
|
||||
<div id="networkStatus" style="color: grey; font-family: montserrat; font-weight: 700; font-size: large;">Tailscale Login</div>
|
||||
</a>
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div style="flex-grow:0; flex-shrink: 0; height:1px; width: 100%; background-color: white;">
|
||||
</div>
|
||||
<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;" class="scrollbar" 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. |",
|
||||
"| |",
|
||||
"| WebVM is powered by the CheerpX virtualization engine, which 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. |",
|
||||
"| |",
|
||||
"| [NEW!] WebVM now supports full TCP and UDP networking via Tailscale! |",
|
||||
"| Click on 'Tailscale Login' to enable it. Read the announcement: |",
|
||||
"| |",
|
||||
"| " + underline + "https://leaningtech.com/webvm-virtual-machine-with-networking-via-tailscale" + 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",
|
||||
" curl --max-time 15 parrot.live # requires networking",
|
||||
"",
|
||||
];
|
||||
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:"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_20221004.ext2",name:"block1"}],mounts:[{type:"ext2",dev:"block1",path:"/"},{type:"cheerpOS",dev:"/app",path:"/app"},{type:"cheerpOS",dev:"/str",path:"/data"},{type:"devs",dev:"",path:"/dev"}], networkInterface}).then(runTest, failCallback);
|
||||
}
|
||||
function initialMessage()
|
||||
{
|
||||
printOnTerm.printMessage(printOnTerm.getAsciiTitle());
|
||||
printOnTerm.printMessage([""]);
|
||||
printOnTerm.printMessage(printOnTerm.getAsciiText());
|
||||
term.registerLinkMatcher(/https:\/\/leaningtech.com\/webvm-virtual-machine-with-networking-via-tailscale/, function(mouseEvent, matchedString) {
|
||||
window.open(matchedString, "_blank")
|
||||
});
|
||||
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>
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script defer src="https://www.googletagmanager.com/gtag/js?id=G-818T3Y0PEY"></script>
|
||||
<script defer>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-818T3Y0PEY');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
85
network.js
|
@ -1,85 +0,0 @@
|
|||
import { State } from "./tun/tailscale_tun.js";
|
||||
import { autoConf } from "./tun/tailscale_tun_auto.js";
|
||||
|
||||
let params = new URLSearchParams("?"+window.location.hash.substr(1));
|
||||
let authKey = params.get("authKey") || undefined;
|
||||
let controlUrl = params.get("controlUrl") || undefined;
|
||||
console.log(authKey, controlUrl);
|
||||
let loginElemUrl = controlUrl ? null : "https://login.tailscale.com/admin/machines";
|
||||
|
||||
let resolveLogin = null;
|
||||
let loginPromise = new Promise((f,r) => {
|
||||
resolveLogin = f;
|
||||
});
|
||||
const loginElem = document.getElementById("loginLink");
|
||||
const statusElem = document.getElementById("networkStatus");
|
||||
const loginUrlCb = (url) => {
|
||||
loginElem.href = url;
|
||||
loginElem.target = "_blank";
|
||||
statusElem.innerHTML = "Tailscale Login";
|
||||
resolveLogin(url);
|
||||
};
|
||||
const stateUpdateCb = (state) => {
|
||||
switch(state)
|
||||
{
|
||||
case State.NeedsLogin:
|
||||
{
|
||||
break;
|
||||
}
|
||||
case State.Running:
|
||||
{
|
||||
if (loginElemUrl) {
|
||||
loginElem.href = loginElemUrl;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case State.Starting:
|
||||
{
|
||||
break;
|
||||
}
|
||||
case State.Stopped:
|
||||
{
|
||||
break;
|
||||
}
|
||||
case State.NoState:
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
const netmapUpdateCb = (map) => {
|
||||
const ip = map.self.addresses[0];
|
||||
statusElem.innerHTML = "IP: "+ip;
|
||||
};
|
||||
const { tcpSocket, udpSocket, up } = await autoConf({
|
||||
loginUrlCb,
|
||||
stateUpdateCb,
|
||||
netmapUpdateCb,
|
||||
authKey,
|
||||
controlUrl,
|
||||
});
|
||||
window.networkInterface.tcpSocket = tcpSocket;
|
||||
window.networkInterface.udpSocket = udpSocket;
|
||||
window.networkInterface.ready = true;
|
||||
loginElem.style.cursor = "pointer";
|
||||
statusElem.style.color = "white";
|
||||
if (authKey) {
|
||||
if (loginElemUrl) {
|
||||
loginElem.href = loginElemUrl;
|
||||
loginElem.target = "_blank";
|
||||
}
|
||||
up();
|
||||
} else {
|
||||
loginElem.onclick = () => {
|
||||
loginElem.onclick = null;
|
||||
statusElem.innerHTML = "Downloading network code...";
|
||||
const w = window.open("login.html", "_blank");
|
||||
async function waitLogin() {
|
||||
await up();
|
||||
statusElem.innerHTML = "Starting login...";
|
||||
const url = await loginPromise;
|
||||
w.location.href = url;
|
||||
}
|
||||
waitLogin();
|
||||
};
|
||||
}
|
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;
|
||||
}}
|
||||
},
|
||||
}
|
|
@ -55,9 +55,14 @@ async function doRegister() {
|
|||
// f.e on first access.
|
||||
registration.addEventListener("updatefound", () => {
|
||||
console.log("Reloading the page to transfer control to the Service Worker.");
|
||||
window.location.reload();
|
||||
try {
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
console.log("Service Worker failed reloading the page. ERROR:" + err);
|
||||
};
|
||||
});
|
||||
// If the registration is active, but it's not controlling the page, reload the page to have it take control
|
||||
// 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 {
|
||||
|
|
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: [],
|
||||
}
|
||||
|
275
tinycore.html
|
@ -1,275 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<!-- Serviceworker script that adds the COI and CORS headers to the response headers in cases where the server does not support it. -->
|
||||
<script src="serviceWorker.js"></script>
|
||||
|
||||
<html lang="en" style="height:100%;">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>WebVM - Linux virtualization in WebAssembly</title>
|
||||
|
||||
<meta name="description" content="Server-less virtual machine, networking included, running browser-side in HTML5/WebAssembly. Code in any programming language inside this Linux terminal.">
|
||||
<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:url" content="/">
|
||||
<meta property="og:image" content="https://webvm.io/assets/welcome_to_WebVM_.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="Server-less virtual machine, networking included, running browser-side in HTML5/WebAssembly. Code in any programming language inside this Linux terminal.">
|
||||
<meta name="twitter:image" content="https://webvm.io/assets/welcome_to_WebVM_.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" id="us-fonts-css" href="https://fonts.googleapis.com/css?family=Montserrat%3A300%2C400%2C500%2C600%2C700&display=swap&ver=6.0.2" media="all">
|
||||
<link rel="stylesheet" href="./xterm/xterm.css" />
|
||||
<link rel="stylesheet" href="./scrollbar.css" />
|
||||
<script src="./xterm/xterm.js"></script>
|
||||
<script src="./xterm/xterm-addon-fit.js"></script>
|
||||
<script>
|
||||
window.networkInterface = { ready: false };
|
||||
</script>
|
||||
<script type="module" src="network.js"></script>
|
||||
</head>
|
||||
|
||||
<body style="margin:0;height:100%;background:black;color:white;overflow:hidden; display:flex; flex-direction: column; justify-content: space-between; height: 100%;">
|
||||
|
||||
<header style="flex-grow:0; flex-srink: 0;height:80px; width: 100%; margin: 2px 0 2px 0;">
|
||||
<div style="display: flex; flex-direction: row; justify-content: space-between; width: 100%;">
|
||||
<div style="display: flex; flex-direction: row;">
|
||||
<a href="https://leaningtech.com/" target="_blank">
|
||||
<img src="./assets/leaningtech.png" style="margin-left: 10px; height: 60px; margin-top: 10px;">
|
||||
</a>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: row; justify-content: space-before;">
|
||||
<li style=" margin-right: 50px; height: 100%; display: flex; align-items: center;">
|
||||
<a href="https://discord.leaningtech.com" target="_blank" style="text-decoration: none">
|
||||
<div style="color: white; font-family: montserrat; font-weight: 700; font-size: large;">Discord</div>
|
||||
</a>
|
||||
</li>
|
||||
<li style=" margin-right: 50px; height: 100%; display: flex; align-items: center;">
|
||||
<a href="https://github.com/leaningtech/webvm" target="_blank" style="text-decoration: none" >
|
||||
<div style="color: white; font-family: montserrat; font-weight: 700; font-size: large;">Github</div>
|
||||
</a>
|
||||
</li>
|
||||
<li style=" margin-right: 50px; height: 100%; display: flex; align-items: center;">
|
||||
<a id="loginLink" style="text-decoration: none; cursor:not-allowed;">
|
||||
<div id="networkStatus" style="color: grey; font-family: montserrat; font-weight: 700; font-size: large;">Tailscale Login</div>
|
||||
</a>
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div style="flex-grow:0; flex-shrink: 0; height:1px; width: 100%; background-color: white;">
|
||||
</div>
|
||||
<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;" class="scrollbar" 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. |",
|
||||
"| |",
|
||||
"| WebVM is powered by the CheerpX virtualization engine, which 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. |",
|
||||
"| |",
|
||||
"| [NEW!] WebVM now supports full TCP and UDP networking via Tailscale! |",
|
||||
"| Click on 'Tailscale Login' to enable it. Read the announcement: |",
|
||||
"| |",
|
||||
"| " + underline + "https://leaningtech.com/webvm-virtual-machine-with-networking-via-tailscale" + 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",
|
||||
" curl --max-time 15 parrot.live # requires networking",
|
||||
"",
|
||||
];
|
||||
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:"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 init specific logic
|
||||
function runInit()
|
||||
{
|
||||
if (typeof SharedArrayBuffer === "undefined")
|
||||
{
|
||||
printOnTerm.printError(printOnTerm.getSharedArrayBufferMissingMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
async function runTest(cx)
|
||||
{
|
||||
term.scrollToBottom();
|
||||
|
||||
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 = {uid:0};
|
||||
cx.run("/init", [], opts);
|
||||
}
|
||||
function failCallback(err)
|
||||
{
|
||||
printOnTerm.printError(printOnTerm.getErrorMessage(err));
|
||||
}
|
||||
CheerpXApp.create({devices:[{type:"block",url:"https://disks.leaningtech.com/tc_nographic_20221117.ext2",name:"block1"}],mounts:[{type:"ext2",dev:"block1",path:"/"},{type:"cheerpOS",dev:"/app",path:"/app"}], networkInterface}).then(runTest, failCallback);
|
||||
}
|
||||
function initialMessage()
|
||||
{
|
||||
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", runInit, false);
|
||||
|
||||
document.head.appendChild(script);
|
||||
|
||||
</script>
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script defer src="https://www.googletagmanager.com/gtag/js?id=G-818T3Y0PEY"></script>
|
||||
<script defer>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-818T3Y0PEY');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
1372
tun/ipstack.js
BIN
tun/ipstack.wasm
|
@ -1,107 +0,0 @@
|
|||
import "./wasm_exec.js";
|
||||
|
||||
import ipStackAwait from "./ipstack.js";
|
||||
|
||||
export const State = {
|
||||
NoState: 0,
|
||||
InUseOtherUser: 1,
|
||||
NeedsLogin: 2,
|
||||
NeedsMachineAuth: 3,
|
||||
Stopped: 4,
|
||||
Starting: 5,
|
||||
Running: 6,
|
||||
};
|
||||
|
||||
export async function init() {
|
||||
const {IpStack} = await ipStackAwait();
|
||||
IpStack.init();
|
||||
|
||||
const listeners = {
|
||||
onstateupdate: () => {},
|
||||
onnetmap: () => {},
|
||||
onloginurl: () => {},
|
||||
}
|
||||
|
||||
let ipn = null;
|
||||
let localIp = null;
|
||||
let dnsIp = null;
|
||||
|
||||
const lazyRunIpn = async () => {
|
||||
const wasmUrl = new URL("tailscale.wasm", import.meta.url);
|
||||
const go = new window.Go();
|
||||
let {instance} = await fetch(wasmUrl).then(x => x.arrayBuffer()).then(x => WebAssembly.instantiate(x,go.importObject));
|
||||
go.run(instance);
|
||||
|
||||
const sessionStateStorage = {
|
||||
setState(id, value) {
|
||||
window.sessionStorage[`ipn-state-${id}`] = value
|
||||
},
|
||||
getState(id) {
|
||||
return window.sessionStorage[`ipn-state-${id}`] || ""
|
||||
},
|
||||
}
|
||||
ipn = newIPN({
|
||||
// Persist IPN state in sessionStorage in development, so that we don't need
|
||||
// to re-authorize every time we reload the page.
|
||||
//stateStorage: sessionStateStorage,
|
||||
});
|
||||
|
||||
const setupIpStack = () => {
|
||||
ipn.tun.onmessage = function(ev) {
|
||||
IpStack.input(ev.data)
|
||||
};
|
||||
IpStack.output(function(p){
|
||||
ipn.tun.postMessage(p, [p.buffer]);
|
||||
});
|
||||
};
|
||||
setupIpStack();
|
||||
|
||||
ipn.run({
|
||||
notifyState: (s) => listeners.onstateupdate(s),
|
||||
notifyNetMap: (s) => {
|
||||
const netMap = JSON.parse(s);
|
||||
listeners.onnetmap(netMap);
|
||||
const newLocalIp = netMap.self.addresses[0];
|
||||
if (localIp != newLocalIp)
|
||||
{
|
||||
localIp = newLocalIp;
|
||||
try{
|
||||
IpStack.up({localIp, dnsIp, ipMap: {
|
||||
["127.0.0.53"]: dnsIp,
|
||||
[dnsIp]: "127.0.0.53",
|
||||
}});
|
||||
}catch(e){
|
||||
console.log(e);
|
||||
debugger;
|
||||
}
|
||||
}
|
||||
},
|
||||
notifyBrowseToURL: (l) => listeners.onloginurl(l),
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
tcpSocket: IpStack.TCPSocket.create,
|
||||
udpSocket: IpStack.UDPSocket.create,
|
||||
parseIP: IpStack.parseIP,
|
||||
resolve: IpStack.resolve,
|
||||
up: async (conf) => {
|
||||
if (ipn == null) {
|
||||
await lazyRunIpn();
|
||||
}
|
||||
ipn.up(conf);
|
||||
localIp = null;
|
||||
dnsIp = conf.dnsIp || "127.0.0.53";
|
||||
},
|
||||
down: () => {
|
||||
ipn.down();
|
||||
IpStack.down();
|
||||
},
|
||||
login: () => ipn.login(),
|
||||
logout: () => ipn.logout(),
|
||||
listeners
|
||||
};
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
import {State, init} from "./tailscale_tun.js";
|
||||
|
||||
export async function autoConf({loginUrlCb, stateUpdateCb, netmapUpdateCb, controlUrl, authKey}) {
|
||||
const { tcpSocket, udpSocket, parseIP, resolve, up, down, login, logout, listeners } = await init();
|
||||
|
||||
const settings = {
|
||||
controlUrl: controlUrl,
|
||||
authKey: authKey,
|
||||
exitNodeIp: undefined,
|
||||
dnsIp: undefined,
|
||||
wantsRunning: true,
|
||||
};
|
||||
|
||||
listeners.onstateupdate = (state) => {
|
||||
stateUpdateCb(state);
|
||||
switch(state)
|
||||
{
|
||||
case State.NeedsLogin:
|
||||
{
|
||||
login();
|
||||
break;
|
||||
}
|
||||
case State.Running:
|
||||
{
|
||||
break;
|
||||
}
|
||||
case State.Starting:
|
||||
{
|
||||
break;
|
||||
}
|
||||
case State.Stopped:
|
||||
{
|
||||
break;
|
||||
}
|
||||
case State.NoState:
|
||||
{
|
||||
up(settings);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
console.log(state);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
listeners.onloginurl = (login) => {
|
||||
console.log("login url:",login);
|
||||
loginUrlCb(login);
|
||||
};
|
||||
|
||||
listeners.onnetmap = (map) => {
|
||||
netmapUpdateCb(map);
|
||||
if (!settings.exitNodeIp) {
|
||||
for (let p of map.peers) {
|
||||
if (p.online && p.exitNode) {
|
||||
settings.exitNodeIp = p.addresses[0];
|
||||
settings.dnsIp = "8.8.8.8";
|
||||
up(settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
tcpSocket,
|
||||
udpSocket,
|
||||
parseIP,
|
||||
resolve,
|
||||
up: async () => {
|
||||
await up(settings);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
554
tun/wasm_exec.js
|
@ -1,554 +0,0 @@
|
|||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
"use strict";
|
||||
|
||||
(() => {
|
||||
const enosys = () => {
|
||||
const err = new Error("not implemented");
|
||||
err.code = "ENOSYS";
|
||||
return err;
|
||||
};
|
||||
|
||||
if (!globalThis.fs) {
|
||||
let outputBuf = "";
|
||||
globalThis.fs = {
|
||||
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
|
||||
writeSync(fd, buf) {
|
||||
outputBuf += decoder.decode(buf);
|
||||
const nl = outputBuf.lastIndexOf("\n");
|
||||
if (nl != -1) {
|
||||
console.log(outputBuf.substr(0, nl));
|
||||
outputBuf = outputBuf.substr(nl + 1);
|
||||
}
|
||||
return buf.length;
|
||||
},
|
||||
write(fd, buf, offset, length, position, callback) {
|
||||
if (offset !== 0 || length !== buf.length || position !== null) {
|
||||
callback(enosys());
|
||||
return;
|
||||
}
|
||||
const n = this.writeSync(fd, buf);
|
||||
callback(null, n);
|
||||
},
|
||||
chmod(path, mode, callback) { callback(enosys()); },
|
||||
chown(path, uid, gid, callback) { callback(enosys()); },
|
||||
close(fd, callback) { callback(enosys()); },
|
||||
fchmod(fd, mode, callback) { callback(enosys()); },
|
||||
fchown(fd, uid, gid, callback) { callback(enosys()); },
|
||||
fstat(fd, callback) { callback(enosys()); },
|
||||
fsync(fd, callback) { callback(null); },
|
||||
ftruncate(fd, length, callback) { callback(enosys()); },
|
||||
lchown(path, uid, gid, callback) { callback(enosys()); },
|
||||
link(path, link, callback) { callback(enosys()); },
|
||||
lstat(path, callback) { callback(enosys()); },
|
||||
mkdir(path, perm, callback) { callback(enosys()); },
|
||||
open(path, flags, mode, callback) { callback(enosys()); },
|
||||
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
|
||||
readdir(path, callback) { callback(enosys()); },
|
||||
readlink(path, callback) { callback(enosys()); },
|
||||
rename(from, to, callback) { callback(enosys()); },
|
||||
rmdir(path, callback) { callback(enosys()); },
|
||||
stat(path, callback) { callback(enosys()); },
|
||||
symlink(path, link, callback) { callback(enosys()); },
|
||||
truncate(path, length, callback) { callback(enosys()); },
|
||||
unlink(path, callback) { callback(enosys()); },
|
||||
utimes(path, atime, mtime, callback) { callback(enosys()); },
|
||||
};
|
||||
}
|
||||
|
||||
if (!globalThis.process) {
|
||||
globalThis.process = {
|
||||
getuid() { return -1; },
|
||||
getgid() { return -1; },
|
||||
geteuid() { return -1; },
|
||||
getegid() { return -1; },
|
||||
getgroups() { throw enosys(); },
|
||||
pid: -1,
|
||||
ppid: -1,
|
||||
umask() { throw enosys(); },
|
||||
cwd() { throw enosys(); },
|
||||
chdir() { throw enosys(); },
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalThis.crypto) {
|
||||
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
|
||||
}
|
||||
|
||||
if (!globalThis.performance) {
|
||||
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
|
||||
}
|
||||
|
||||
if (!globalThis.TextEncoder) {
|
||||
throw new Error("globalThis.TextEncoder is not available, polyfill required");
|
||||
}
|
||||
|
||||
if (!globalThis.TextDecoder) {
|
||||
throw new Error("globalThis.TextDecoder is not available, polyfill required");
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder("utf-8");
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
|
||||
globalThis.Go = class {
|
||||
constructor() {
|
||||
this.argv = ["js"];
|
||||
this.env = {};
|
||||
this.exit = (code) => {
|
||||
if (code !== 0) {
|
||||
console.warn("exit code:", code);
|
||||
}
|
||||
};
|
||||
this._exitPromise = new Promise((resolve) => {
|
||||
this._resolveExitPromise = resolve;
|
||||
});
|
||||
this._pendingEvent = null;
|
||||
this._scheduledTimeouts = new Map();
|
||||
this._nextCallbackTimeoutID = 1;
|
||||
|
||||
const setInt64 = (addr, v) => {
|
||||
this.mem.setUint32(addr + 0, v, true);
|
||||
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
|
||||
}
|
||||
|
||||
const getInt64 = (addr) => {
|
||||
const low = this.mem.getUint32(addr + 0, true);
|
||||
const high = this.mem.getInt32(addr + 4, true);
|
||||
return low + high * 4294967296;
|
||||
}
|
||||
|
||||
const loadValue = (addr) => {
|
||||
const f = this.mem.getFloat64(addr, true);
|
||||
if (f === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (!isNaN(f)) {
|
||||
return f;
|
||||
}
|
||||
|
||||
const id = this.mem.getUint32(addr, true);
|
||||
return this._values[id];
|
||||
}
|
||||
|
||||
const storeValue = (addr, v) => {
|
||||
const nanHead = 0x7FF80000;
|
||||
|
||||
if (typeof v === "number" && v !== 0) {
|
||||
if (isNaN(v)) {
|
||||
this.mem.setUint32(addr + 4, nanHead, true);
|
||||
this.mem.setUint32(addr, 0, true);
|
||||
return;
|
||||
}
|
||||
this.mem.setFloat64(addr, v, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (v === undefined) {
|
||||
this.mem.setFloat64(addr, 0, true);
|
||||
return;
|
||||
}
|
||||
|
||||
let id = this._ids.get(v);
|
||||
if (id === undefined) {
|
||||
id = this._idPool.pop();
|
||||
if (id === undefined) {
|
||||
id = this._values.length;
|
||||
}
|
||||
this._values[id] = v;
|
||||
this._goRefCounts[id] = 0;
|
||||
this._ids.set(v, id);
|
||||
}
|
||||
this._goRefCounts[id]++;
|
||||
let typeFlag = 0;
|
||||
switch (typeof v) {
|
||||
case "object":
|
||||
if (v !== null) {
|
||||
typeFlag = 1;
|
||||
}
|
||||
break;
|
||||
case "string":
|
||||
typeFlag = 2;
|
||||
break;
|
||||
case "symbol":
|
||||
typeFlag = 3;
|
||||
break;
|
||||
case "function":
|
||||
typeFlag = 4;
|
||||
break;
|
||||
}
|
||||
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
|
||||
this.mem.setUint32(addr, id, true);
|
||||
}
|
||||
|
||||
const loadSlice = (addr) => {
|
||||
const array = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
|
||||
}
|
||||
|
||||
const loadSliceOfValues = (addr) => {
|
||||
const array = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
const a = new Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
a[i] = loadValue(array + i * 8);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
const loadString = (addr) => {
|
||||
const saddr = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
|
||||
}
|
||||
|
||||
const timeOrigin = Date.now() - performance.now();
|
||||
this.importObject = {
|
||||
go: {
|
||||
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
|
||||
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
|
||||
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
|
||||
// This changes the SP, thus we have to update the SP used by the imported function.
|
||||
|
||||
// func wasmExit(code int32)
|
||||
"runtime.wasmExit": (sp) => {
|
||||
sp >>>= 0;
|
||||
const code = this.mem.getInt32(sp + 8, true);
|
||||
this.exited = true;
|
||||
delete this._inst;
|
||||
delete this._values;
|
||||
delete this._goRefCounts;
|
||||
delete this._ids;
|
||||
delete this._idPool;
|
||||
this.exit(code);
|
||||
},
|
||||
|
||||
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
|
||||
"runtime.wasmWrite": (sp) => {
|
||||
sp >>>= 0;
|
||||
const fd = getInt64(sp + 8);
|
||||
const p = getInt64(sp + 16);
|
||||
const n = this.mem.getInt32(sp + 24, true);
|
||||
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
|
||||
},
|
||||
|
||||
// func resetMemoryDataView()
|
||||
"runtime.resetMemoryDataView": (sp) => {
|
||||
sp >>>= 0;
|
||||
this.mem = new DataView(this._inst.exports.mem.buffer);
|
||||
},
|
||||
|
||||
// func nanotime1() int64
|
||||
"runtime.nanotime1": (sp) => {
|
||||
sp >>>= 0;
|
||||
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
|
||||
},
|
||||
|
||||
// func walltime() (sec int64, nsec int32)
|
||||
"runtime.walltime": (sp) => {
|
||||
sp >>>= 0;
|
||||
const msec = (new Date).getTime();
|
||||
setInt64(sp + 8, msec / 1000);
|
||||
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
|
||||
},
|
||||
|
||||
// func scheduleTimeoutEvent(delay int64) int32
|
||||
"runtime.scheduleTimeoutEvent": (sp) => {
|
||||
sp >>>= 0;
|
||||
const id = this._nextCallbackTimeoutID;
|
||||
this._nextCallbackTimeoutID++;
|
||||
this._scheduledTimeouts.set(id, setTimeout(
|
||||
() => {
|
||||
this._resume();
|
||||
while (this._scheduledTimeouts.has(id)) {
|
||||
// for some reason Go failed to register the timeout event, log and try again
|
||||
// (temporary workaround for https://github.com/golang/go/issues/28975)
|
||||
console.warn("scheduleTimeoutEvent: missed timeout event");
|
||||
this._resume();
|
||||
}
|
||||
},
|
||||
getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early
|
||||
));
|
||||
this.mem.setInt32(sp + 16, id, true);
|
||||
},
|
||||
|
||||
// func clearTimeoutEvent(id int32)
|
||||
"runtime.clearTimeoutEvent": (sp) => {
|
||||
sp >>>= 0;
|
||||
const id = this.mem.getInt32(sp + 8, true);
|
||||
clearTimeout(this._scheduledTimeouts.get(id));
|
||||
this._scheduledTimeouts.delete(id);
|
||||
},
|
||||
|
||||
// func getRandomData(r []byte)
|
||||
"runtime.getRandomData": (sp) => {
|
||||
sp >>>= 0;
|
||||
crypto.getRandomValues(loadSlice(sp + 8));
|
||||
},
|
||||
|
||||
// func finalizeRef(v ref)
|
||||
"syscall/js.finalizeRef": (sp) => {
|
||||
sp >>>= 0;
|
||||
const id = this.mem.getUint32(sp + 8, true);
|
||||
this._goRefCounts[id]--;
|
||||
if (this._goRefCounts[id] === 0) {
|
||||
const v = this._values[id];
|
||||
this._values[id] = null;
|
||||
this._ids.delete(v);
|
||||
this._idPool.push(id);
|
||||
}
|
||||
},
|
||||
|
||||
// func stringVal(value string) ref
|
||||
"syscall/js.stringVal": (sp) => {
|
||||
sp >>>= 0;
|
||||
storeValue(sp + 24, loadString(sp + 8));
|
||||
},
|
||||
|
||||
// func valueGet(v ref, p string) ref
|
||||
"syscall/js.valueGet": (sp) => {
|
||||
sp >>>= 0;
|
||||
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 32, result);
|
||||
},
|
||||
|
||||
// func valueSet(v ref, p string, x ref)
|
||||
"syscall/js.valueSet": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
|
||||
},
|
||||
|
||||
// func valueDelete(v ref, p string)
|
||||
"syscall/js.valueDelete": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
|
||||
},
|
||||
|
||||
// func valueIndex(v ref, i int) ref
|
||||
"syscall/js.valueIndex": (sp) => {
|
||||
sp >>>= 0;
|
||||
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
|
||||
},
|
||||
|
||||
// valueSetIndex(v ref, i int, x ref)
|
||||
"syscall/js.valueSetIndex": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
|
||||
},
|
||||
|
||||
// func valueCall(v ref, m string, args []ref) (ref, bool)
|
||||
"syscall/js.valueCall": (sp) => {
|
||||
sp >>>= 0;
|
||||
try {
|
||||
const v = loadValue(sp + 8);
|
||||
const m = Reflect.get(v, loadString(sp + 16));
|
||||
const args = loadSliceOfValues(sp + 32);
|
||||
const result = Reflect.apply(m, v, args);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 56, result);
|
||||
this.mem.setUint8(sp + 64, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 56, err);
|
||||
this.mem.setUint8(sp + 64, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueInvoke(v ref, args []ref) (ref, bool)
|
||||
"syscall/js.valueInvoke": (sp) => {
|
||||
sp >>>= 0;
|
||||
try {
|
||||
const v = loadValue(sp + 8);
|
||||
const args = loadSliceOfValues(sp + 16);
|
||||
const result = Reflect.apply(v, undefined, args);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, result);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, err);
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueNew(v ref, args []ref) (ref, bool)
|
||||
"syscall/js.valueNew": (sp) => {
|
||||
sp >>>= 0;
|
||||
try {
|
||||
const v = loadValue(sp + 8);
|
||||
const args = loadSliceOfValues(sp + 16);
|
||||
const result = Reflect.construct(v, args);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, result);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, err);
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueLength(v ref) int
|
||||
"syscall/js.valueLength": (sp) => {
|
||||
sp >>>= 0;
|
||||
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
|
||||
},
|
||||
|
||||
// valuePrepareString(v ref) (ref, int)
|
||||
"syscall/js.valuePrepareString": (sp) => {
|
||||
sp >>>= 0;
|
||||
const str = encoder.encode(String(loadValue(sp + 8)));
|
||||
storeValue(sp + 16, str);
|
||||
setInt64(sp + 24, str.length);
|
||||
},
|
||||
|
||||
// valueLoadString(v ref, b []byte)
|
||||
"syscall/js.valueLoadString": (sp) => {
|
||||
sp >>>= 0;
|
||||
const str = loadValue(sp + 8);
|
||||
loadSlice(sp + 16).set(str);
|
||||
},
|
||||
|
||||
// func valueInstanceOf(v ref, t ref) bool
|
||||
"syscall/js.valueInstanceOf": (sp) => {
|
||||
sp >>>= 0;
|
||||
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
|
||||
},
|
||||
|
||||
// func copyBytesToGo(dst []byte, src ref) (int, bool)
|
||||
"syscall/js.copyBytesToGo": (sp) => {
|
||||
sp >>>= 0;
|
||||
const dst = loadSlice(sp + 8);
|
||||
const src = loadValue(sp + 32);
|
||||
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
return;
|
||||
}
|
||||
const toCopy = src.subarray(0, dst.length);
|
||||
dst.set(toCopy);
|
||||
setInt64(sp + 40, toCopy.length);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
},
|
||||
|
||||
// func copyBytesToJS(dst ref, src []byte) (int, bool)
|
||||
"syscall/js.copyBytesToJS": (sp) => {
|
||||
sp >>>= 0;
|
||||
const dst = loadValue(sp + 8);
|
||||
const src = loadSlice(sp + 16);
|
||||
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
return;
|
||||
}
|
||||
const toCopy = src.subarray(0, dst.length);
|
||||
dst.set(toCopy);
|
||||
setInt64(sp + 40, toCopy.length);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
},
|
||||
|
||||
"debug": (value) => {
|
||||
console.log(value);
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async run(instance) {
|
||||
if (!(instance instanceof WebAssembly.Instance)) {
|
||||
throw new Error("Go.run: WebAssembly.Instance expected");
|
||||
}
|
||||
this._inst = instance;
|
||||
this.mem = new DataView(this._inst.exports.mem.buffer);
|
||||
this._values = [ // JS values that Go currently has references to, indexed by reference id
|
||||
NaN,
|
||||
0,
|
||||
null,
|
||||
true,
|
||||
false,
|
||||
globalThis,
|
||||
this,
|
||||
];
|
||||
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
|
||||
this._ids = new Map([ // mapping from JS values to reference ids
|
||||
[0, 1],
|
||||
[null, 2],
|
||||
[true, 3],
|
||||
[false, 4],
|
||||
[globalThis, 5],
|
||||
[this, 6],
|
||||
]);
|
||||
this._idPool = []; // unused ids that have been garbage collected
|
||||
this.exited = false; // whether the Go program has exited
|
||||
|
||||
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
|
||||
let offset = 4096;
|
||||
|
||||
const strPtr = (str) => {
|
||||
const ptr = offset;
|
||||
const bytes = encoder.encode(str + "\0");
|
||||
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
|
||||
offset += bytes.length;
|
||||
if (offset % 8 !== 0) {
|
||||
offset += 8 - (offset % 8);
|
||||
}
|
||||
return ptr;
|
||||
};
|
||||
|
||||
const argc = this.argv.length;
|
||||
|
||||
const argvPtrs = [];
|
||||
this.argv.forEach((arg) => {
|
||||
argvPtrs.push(strPtr(arg));
|
||||
});
|
||||
argvPtrs.push(0);
|
||||
|
||||
const keys = Object.keys(this.env).sort();
|
||||
keys.forEach((key) => {
|
||||
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
|
||||
});
|
||||
argvPtrs.push(0);
|
||||
|
||||
const argv = offset;
|
||||
argvPtrs.forEach((ptr) => {
|
||||
this.mem.setUint32(offset, ptr, true);
|
||||
this.mem.setUint32(offset + 4, 0, true);
|
||||
offset += 8;
|
||||
});
|
||||
|
||||
// The linker guarantees global data starts from at least wasmMinDataAddr.
|
||||
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
|
||||
const wasmMinDataAddr = 4096 + 8192;
|
||||
if (offset >= wasmMinDataAddr) {
|
||||
throw new Error("total length of command line and environment variables exceeds limit");
|
||||
}
|
||||
|
||||
this._inst.exports.run(argc, argv);
|
||||
if (this.exited) {
|
||||
this._resolveExitPromise();
|
||||
}
|
||||
await this._exitPromise;
|
||||
}
|
||||
|
||||
_resume() {
|
||||
if (this.exited) {
|
||||
throw new Error("Go program has already exited");
|
||||
}
|
||||
this._inst.exports.resume();
|
||||
if (this.exited) {
|
||||
this._resolveExitPromise();
|
||||
}
|
||||
}
|
||||
|
||||
_makeFuncWrapper(id) {
|
||||
const go = this;
|
||||
return function () {
|
||||
const event = { id: id, this: this, args: arguments };
|
||||
go._pendingEvent = event;
|
||||
go._resume();
|
||||
return event.result;
|
||||
};
|
||||
}
|
||||
}
|
||||
})();
|
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 +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)})()}));
|
||||
!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;
|
||||
}
|
||||
|
|