Compare commits

..

1 commit

Author SHA1 Message Date
elisabeth
d05d6cc654 Set the root and user passwords to password in the Dockerfiles 2023-05-22 12:14:53 +02:00
56 changed files with 514 additions and 4432 deletions

View file

@ -1,42 +0,0 @@
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

View file

@ -33,62 +33,7 @@ on:
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:
@ -98,10 +43,23 @@ jobs:
permissions: # Permissions to grant the GITHUB_TOKEN.
contents: write # Required permission to make a github release.
actions: 'write' # Required for the Dockerfile path guard clause.
steps:
# Checks-out our repository under $GITHUB_WORKSPACE, so our job can access it
- uses: actions/checkout@v3
# Guard clause that cancels the workflow in case of an invalid DOCKERFILE_PATH.
# 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 guard clause
shell: bash
run: |
if [ ! -f ${{ github.event.inputs.DOCKERFILE_PATH }} ]; then
echo "::error title=Invalid Dockerfile path::No file found at ${{ github.event.inputs.DOCKERFILE_PATH }}"
gh run cancel ${{ github.run_id }}
gh run watch ${{ github.run_id }}
fi
env:
GH_TOKEN: ${{ github.token }} # As required by the GitHub-CLI
# Setting the IMAGE_NAME variable in GITHUB_ENV to <Dockerfile name>_<date>_<run_id>.ext2.
- name: Generate the image_name.
@ -117,33 +75,7 @@ jobs:
# 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
- run: docker run --dns 8.8.8.8 --dns 8.8.4.4 -d $TAG
# We create and mount the base ext2 image to extract the Docker container's filesystem its contents into.
- name: Create ext2 image.
@ -160,40 +92,25 @@ jobs:
# 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 docker cp -a $(sudo docker ps -aq):/ /mnt/
sudo umount /mnt/
# Result is an ext2 image for webvm.
# Move required files for gh-pages deployment to the deployment directory $DEPLOY_DIR.
- run: sudo mv assets examples xterm favicon.ico index.html login.html network.js scrollbar.css serviceWorker.js tower.ico $DEPLOY_DIR
# 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
# This step updates the default index.html file by performing the following actions:
# 2. Replaces all occurrences of IMAGE_URL with the URL to the image.
# 3. Replaces all occurrences of DEVICE_TYPE to bytes.
- name: Adjust index.html
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/
sudo sed -i 's#IMAGE_URL#"${{ env.IMAGE_NAME }}"#g' ${{ env.DEPLOY_DIR }}index.html
sudo sed -i 's#DEVICE_TYPE#"split"#g' ${{ env.DEPLOY_DIR }}index.html
# We generate index.list files for our httpfs to function properly.
- name: make index.list
@ -210,7 +127,7 @@ jobs:
# Create a gh-pages artifact in order to deploy to gh-pages.
- name: Upload GitHub Pages artifact
uses: actions/upload-pages-artifact@v2
uses: actions/upload-pages-artifact@v1.0.7
with:
# Path of the directory containing the static assets for our gh pages deployment.
path: ${{ env.DEPLOY_DIR }} # optional, default is _site/
@ -242,4 +159,4 @@ jobs:
# Deployment to github pages
- name: Deploy GitHub Pages site
id: deployment
uses: actions/deploy-pages@v3
uses: actions/deploy-pages@v2.0.0

1
.npmrc
View file

@ -1 +0,0 @@
engine-strict=true

View file

@ -1,11 +1,8 @@
# WebVM
[![Discord server](https://img.shields.io/discord/988743885121548329?color=%235865F2&logo=discord&logoColor=%23fff)](https://discord.gg/yWRr2YnD9c)
[![Issues](https://img.shields.io/github/issues/leaningtech/webvm)](https://github.com/leaningtech/webvm/issues)
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%">
<img src="assets/welcome_to_WebVM_slim.png" width="95%">
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.
@ -13,12 +10,8 @@ WebVM is powered by the CheerpX virtualization engine, and enables safe, sandbox
# Enable networking
Modern browsers do not provide APIs to directly use TCP or UDP. WebVM provides networking support by integrating with Tailscale, a VPN network that supports WebSockets as a transport layer.
- Open the "Networking" panel from the side-bar
- Click "Connect to Tailscale" from the panel
- Log in to Tailscale (create an account if you don't have one)
- Click "Connect" when prompted by Tailscale
- Click "Connect via Tailscale" in the page header.
- Log in to Tailscale (create an account if you don't have one).
- If you are unfamiliar with Tailscale or would like additional information see [WebVM and Tailscale](/docs/Tailscale.md).
# Fork, deploy, customize
@ -30,7 +23,6 @@ Modern browsers do not provide APIs to directly use TCP or UDP. WebVM provides n
- 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.
@ -41,41 +33,20 @@ Modern browsers do not provide APIs to directly use TCP or UDP. WebVM provides n
<img src="/assets/result.png" width="70%" >
You can now customize `dockerfiles/debian_mini` to suit your needs, or make a new Dockerfile from scratch. Use the `Path to Dockerfile` workflow parameter to select it.
You can now customize `dockerfiles/debian_mini` to suits your needs, or make a new Dockerfile from scratch. Use the "Path to Dockerfile" workflow parameter to select it.
# Local deployment
From a local `git clone`
- Download the `debian_mini` Ext2 image from [https://github.com/leaningtech/webvm/releases/](https://github.com/leaningtech/webvm/releases/)
- You can also build your own by selecting the "Upload GitHub release" workflow option
- Place the image in the repository root folder
- Edit `config_github_terminal.js`
- Uncomment the default values for `CMD`, `ARGS`, `ENV` and `CWD`
- Replace `IMAGE_URL` with the URL (absolute or relative) for the Ext2 image. For example `"/debian_mini_20230519_5022088024.ext2"`
- Build WebVM using `npm`, output will be placed in the `build` directory
- `npm install`
- `npm run build`
- Start NGINX, it automatically points to the `build` directory just created
- `nginx -p . -c nginx.conf`
- Visit `http://127.0.0.1:8081` and enjoy your local WebVM
# Example customization: Python3 REPL
The `Deploy` workflow takes into account the `CMD` specified in the Dockerfile. To build a REPL you can simply apply this patch and deploy.
```diff
diff --git a/dockerfiles/debian_mini b/dockerfiles/debian_mini
index 2878332..1f3103a 100644
--- a/dockerfiles/debian_mini
+++ b/dockerfiles/debian_mini
@@ -15,4 +15,4 @@ WORKDIR /home/user/
# We set env, as this gets extracted by Webvm. This is optional.
ENV HOME="/home/user" TERM="xterm" USER="user" SHELL="/bin/bash" EDITOR="vim" LANG="en_US.UTF-8" LC_ALL="C"
RUN echo 'root:password' | chpasswd
-CMD [ "/bin/bash" ]
+CMD [ "/usr/bin/python3" ]
```
- You can also build your own by selecting the "Upload GitHub release" workflow option.
- Place the image in the repository root folder.
- Edit `index.html`
- Replace `DEVICE_TYPE` with `"bytes"`
- Replace `IMAGE_URL` with the name of the Ext2 image. For example `"debian_mini_20230519_5022088024.ext2"`
- Start a local HTTP server
- Enjoy your local WebVM.
# Bugs and Issues
@ -86,24 +57,23 @@ Or come to say hello / share your feedback on [Discord](https://discord.gg/yTNZg
- [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](https://cheerpx.io/), made by [Leaning Technologies](https://leaningtech.com/) for x86 virtualization and Linux emulation
- CheerpX, made by [Leaning Technologies](https://leaningtech.com) for x86 virtualization and Linux emulation
- xterm.js, [https://xtermjs.org/](https://xtermjs.org/), for providing the Web-based terminal emulator
- [Tailscale](https://tailscale.com/), for the networking component
- [lwIP](https://savannah.nongnu.org/projects/lwip/), for the TCP/IP stack, compiled for the Web via [Cheerp](https://github.com/leaningtech/cheerp-meta/)
- [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).
WebVM depends on the CheerpX x86-to-WebAssembly virtualization technology. A link to the current latest build is always available at [https://cheerpxdemos.leaningtech.com/publicdeploy/LATEST.txt](https://cheerpxdemos.leaningtech.com/publicdeploy/LATEST.txt). Builds of CheerpX are immutable and uniquely versioned. An example link would be:
The NPM package is updated on every release.
`https://cheerpxdemos.leaningtech.com/publicdeploy/20230517_94/cx.js`
Every build is immutable, if a specific version works well for you today, it will keep working forever.
We strongly encourage users _not_ to use the latest build. Plase directly use a specific build to avoid unexpected regressions. Since builds are immutable, if they work for you now they will keep working forever.
# License
@ -111,8 +81,6 @@ 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)
The public CheerpX deployment is provided **as-is** and is **free to use** for technological exploration, testing and non-commercial uses. Downloading a CheerpX build for the purpose of hosting it elsewhere is not permitted.
If you want to build a product on top of CheerpX/WebVM, please get in touch: sales@leaningtech.com

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

View file

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 514 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View file

@ -1,23 +0,0 @@
// 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
};

View file

@ -1,19 +0,0 @@
// 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
};

View file

@ -1,23 +0,0 @@
// 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
};

View file

@ -10,9 +10,5 @@ RUN apt-get -y install apt-utils gcc \
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" ]

View file

@ -7,7 +7,7 @@
**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.
- `Connect via Tailscale` should be replaced by your IP address.
# Log in to Tailscale with an Auth key

Binary file not shown.

Before

Width:  |  Height:  |  Size: 359 KiB

Binary file not shown.

View file

@ -1,5 +0,0 @@
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

View file

@ -1,3 +0,0 @@
ArchitectureOverview.png
WebAssemblyTools.pdf
Welcome.txt

352
index.html Normal file
View file

@ -0,0 +1,352 @@
<!DOCTYPE html>
<html lang="en" style="height:100%;">
<meta property="og:image" content="https://webvm.io/assets/reddit.png"/>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=1100, initial-scale=1.0">
<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:image" content="https://webvm.io/assets/social.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/social.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&amp;display=swap&amp;ver=6.0.2" media="all">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-fork-ribbon-css/0.2.3/gh-fork-ribbon.min.css" />
<link rel="stylesheet" href="./xterm/xterm.css" />
<link rel="stylesheet" href="./scrollbar.css" />
<style>.github-fork-ribbon:before { background-color: #ea1e69; }</style>
<!-- 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>
<script src="./xterm/xterm.js"></script>
<script src="./xterm/xterm-addon-fit.js"></script>
<script 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%;">
<a class="github-fork-ribbon right-bottom" href="https://github.com/leaningtech/webvm/" target="_blank" data-ribbon="Fork me on GitHub" title="Fork me on GitHub">Fork me on GitHub</a>
<div>
<div style="padding-top: 0.7em;padding-bottom: 0.7em;font-size: 0.3em; font-weight: 200;vertical-align:center;height: 120px;">
<div style="margin-left: 20px; height: 100%; display: flex; align-items: center; justify-content: space-between;">
<pre style="font-family: monospace; font-weight: 600; font-size: large; color: #ad7fa8;">
__ __ _ __ ____ __
\ \ / /__| |_\ \ / / \/ |
\ \/\/ / -_) '_ \ V /| |\/| |
\_/\_/\___|_.__/\_/ |_| |_|
</pre>
<div style="height:100%;display: flex; flex-direction: column;justify-content: space-between;">
<div style="padding-top: 0.7em;font-size: 0.3em; font-weight: 200;vertical-align:center;height:50px;">
<div style="margin-right: 10px; margin-left: 20px; height: 100%; display: flex; align-items: center; justify-content: flex-end;gap: 50px;">
<div style="padding-top: 0.7em;font-size: 0.3em; font-weight: 200;vertical-align:center;">
<a href="https://leaningtech.com" style="text-decoration: none; height: 100%;" target="_blank">
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
<span>Made with &#10084;&#65039; by </span>
<img src="assets/leaningtech.png" height="40px" style="margin-left: 5px;">
</div>
</a>
</div>
</div>
</div>
<div style="padding-top: 0.7em;font-size: 0.3em; font-weight: 200;vertical-align:center;height:50px;">
<div style="margin-right: 10px; margin-left: 20px; height: 100%; display: flex; align-items: center; justify-content: flex-end;gap: 50px;">
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
<span>CPU </span>
<span id="cpuactivity" style="margin-left: 7px;">&#x1F7E2;</span>
</div>
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
<span>HDD </span>
<span id="hddactivity" style="margin-left: 7px;">&#x1F7E2;</span>
</div>
<a id="loginLink" style="text-decoration: none; height: 100%; cursor:not-allowed;">
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
<span id="networkStatus">Connect via Tailscale </span>
<img src="assets/tailscale.svg" height="35px" style="margin-left: 7px;">
</div>
</a>
<a href="https://discord.gg/yTNZgySKGa" style="text-decoration: none; height: 100%;" target="_blank">
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
<span>Join Discord </span>
<img src="assets/discord-mark-blue.svg" height="35px" style="margin-left: 7px;">
</div>
</a>
<a href="https://github.com/leaningtech/webvm/issues" style="text-decoration: none; height: 100%;" target="_blank">
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
<span>GitHub Issues </span>
<img src="assets/github-mark-white.svg" height="35px" style="margin-left: 5px;">
</div>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<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: 5px; 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. 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;
}
let networkInterface = setupNetworkInterface();
async function runTest(cx)
{
registerNetworkLogin(cx, networkInterface);
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", uid: 1000, gid: 1000};
while (true)
{
await cxLogAndRun(cx, structure.cmd, structure.args, opts);
}
}
function failCallback(err)
{
printOnTerm.printError(printOnTerm.getErrorMessage(err));
}
function devCallback(state)
{
var h = document.getElementById("hddactivity");
if(state == "ready")
h.textContent = "\u{1F7E2}";
else
h.textContent = "\u{1F7E0}";
}
function cpuCallback(state)
{
var h = document.getElementById("cpuactivity");
if(state == "ready")
h.textContent = "\u{1F7E2}";
else
h.textContent = "\u{1F7E0}";
}
// The device url and type are replaced by Github Actions.
CheerpXApp.create({devices:[{type:DEVICE_TYPE,url:IMAGE_URL,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: networkInterface, activityInterface: {cpu: cpuCallback, dev: devCallback}}).then(runTest, failCallback);
}
function initialMessage()
{
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();
async function loadCX()
{
// Find the latest build
var r = await fetch("https://cheerpxdemos.leaningtech.com/publicdeploy/LATEST.txt");
var url = await r.text();
url = url.trim();
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.addEventListener("load", runBash, false);
document.head.appendChild(script);
}
loadCX();
</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>

74
network.js Normal file
View file

@ -0,0 +1,74 @@
function setupNetworkInterface()
{
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 6 /*Running*/:
{
if (loginElemUrl) {
loginElem.href = loginElemUrl;
}
break;
}
}
};
const netmapUpdateCb = (map) => {
const ip = map.self.addresses[0];
statusElem.innerHTML = "IP: "+ip;
};
loginElem.style.cursor = "pointer";
statusElem.style.color = "white";
return {
loginUrlCb,
stateUpdateCb,
netmapUpdateCb,
authKey,
controlUrl,
loginElem,
statusElem,
loginElemUrl,
loginPromise,
};
}
function registerNetworkLogin(cx, { authKey, statusElem, loginElem, loginElemUrl, loginPromise })
{
if (authKey) {
if (loginElemUrl) {
loginElem.href = loginElemUrl;
loginElem.target = "_blank";
}
cx.networkLogin();
} else {
loginElem.onclick = () => {
loginElem.onclick = null;
statusElem.innerHTML = "Downloading network code...";
const w = window.open("login.html", "_blank");
async function waitLogin() {
await cx.networkLogin();
statusElem.innerHTML = "Starting login...";
const url = await loginPromise;
w.location.href = url;
}
waitLogin();
};
}
}

View file

@ -32,12 +32,45 @@ http {
# ssl_certificate_key nginx.key;
location / {
root build;
root .;
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

File diff suppressed because it is too large Load diff

View file

@ -1,31 +0,0 @@
{
"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"
}

View file

@ -1,26 +0,0 @@
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;
}}
},
}

View file

@ -1,38 +0,0 @@
<!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>

View file

@ -1,9 +0,0 @@
<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>

View file

@ -1,12 +0,0 @@
<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>

View file

@ -1,12 +0,0 @@
<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>

View file

@ -1,56 +0,0 @@
<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>

View file

@ -1,13 +0,0 @@
<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>

View file

@ -1,20 +0,0 @@
<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>

View file

@ -1,11 +0,0 @@
<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>

View file

@ -1,108 +0,0 @@
<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>

View file

@ -1,19 +0,0 @@
<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>

View file

@ -1,14 +0,0 @@
<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>

View file

@ -1,82 +0,0 @@
<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>

View file

@ -1,341 +0,0 @@
<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>

View file

@ -1,6 +0,0 @@
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);

View file

@ -1,26 +0,0 @@
@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);
}
}

View file

@ -1,51 +0,0 @@
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:",
""
];

View file

@ -1,66 +0,0 @@
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 }

View file

@ -1,44 +0,0 @@
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 };
}

View file

@ -1 +0,0 @@
export const prerender = true;

View file

@ -1,16 +0,0 @@
<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>

View file

@ -1 +0,0 @@
export const prerender = true;

View file

@ -1,16 +0,0 @@
<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>

View file

@ -1,12 +0,0 @@
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;

View file

@ -1,9 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {},
},
plugins: [],
}

View file

@ -1,28 +0,0 @@
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: '' }
]
})
]
});

View file

@ -1,2 +1 @@
!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
!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)})()}));

View file

@ -1,2 +0,0 @@
!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

View file

@ -36,7 +36,6 @@
*/
.xterm {
cursor: text;
position: relative;
user-select: none;
-ms-user-select: none;
@ -125,6 +124,10 @@
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;
@ -140,7 +143,7 @@
cursor: crosshair;
}
.xterm .xterm-accessibility:not(.debug),
.xterm .xterm-accessibility,
.xterm .xterm-message {
position: absolute;
left: 0;
@ -149,16 +152,6 @@
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 {
@ -170,49 +163,13 @@
}
.xterm-dim {
/* 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;
opacity: 0.5;
}
.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-underline {
text-decoration: underline;
}
.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;
}

File diff suppressed because one or more lines are too long