Compare commits
171 commits
api_update
...
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 |
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
|
38
.github/workflows/deploy.yml
vendored
|
@ -164,28 +164,36 @@ jobs:
|
|||
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 index.html file by performing the following actions:
|
||||
|
||||
# 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. Replaces all occurrences of DEVICE_TYPE to bytes.
|
||||
# 3. Replace CMD with the Dockerfile entry command.
|
||||
# 4. Replace args with the Dockerfile CMD / Entrypoint args.
|
||||
# 5. Replace ENV with the container's environment values.
|
||||
- name: Adjust index.html
|
||||
# 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: |
|
||||
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
|
||||
sudo sed -i 's#CMD#${{ env.CMD }}#g' ${{ env.DEPLOY_DIR }}index.html
|
||||
sudo sed -i 's#ARGS#${{ env.ARGS }}#g' ${{ env.DEPLOY_DIR }}index.html
|
||||
sudo sed -i 's#ENV#${{ env.ENV }}#g' ${{ env.DEPLOY_DIR }}index.html
|
||||
sudo sed -i 's#CWD#${{ env.CWD }}#g' ${{ env.DEPLOY_DIR }}index.html
|
||||
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
|
||||
|
|
1
.npmrc
Normal file
|
@ -0,0 +1 @@
|
|||
engine-strict=true
|
47
README.md
|
@ -5,7 +5,7 @@
|
|||
|
||||
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_slim.png" width="95%">
|
||||
<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.
|
||||
|
||||
|
@ -13,8 +13,12 @@ WebVM is powered by the CheerpX virtualization engine, and enables safe, sandbox
|
|||
|
||||
# Enable networking
|
||||
|
||||
- Click "Connect via Tailscale" in the page header.
|
||||
- Log in to Tailscale (create an account if you don't have one).
|
||||
Modern browsers do not provide APIs to directly use TCP or UDP. WebVM provides networking support by integrating with Tailscale, a VPN network that supports WebSockets as a transport layer.
|
||||
|
||||
- Open the "Networking" panel from the side-bar
|
||||
- Click "Connect to Tailscale" from the panel
|
||||
- Log in to Tailscale (create an account if you don't have one)
|
||||
- Click "Connect" when prompted by Tailscale
|
||||
- If you are unfamiliar with Tailscale or would like additional information see [WebVM and Tailscale](/docs/Tailscale.md).
|
||||
|
||||
# Fork, deploy, customize
|
||||
|
@ -37,21 +41,24 @@ WebVM is powered by the CheerpX virtualization engine, and enables safe, sandbox
|
|||
|
||||
<img src="/assets/result.png" width="70%" >
|
||||
|
||||
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.
|
||||
You can now customize `dockerfiles/debian_mini` to suit your needs, or make a new Dockerfile from scratch. Use the `Path to Dockerfile` workflow parameter to select it.
|
||||
|
||||
# Local deployment
|
||||
|
||||
From a local `git clone`
|
||||
|
||||
- Download the `debian_mini` Ext2 image from [https://github.com/leaningtech/webvm/releases/](https://github.com/leaningtech/webvm/releases/).
|
||||
- You can also build your own by selecting the "Upload GitHub release" workflow option.
|
||||
- Place the image in the repository root folder.
|
||||
- Edit `index.html`.
|
||||
- Uncomment the default values for `CMD`, `ARGS`, `ENV` and `CWD`.
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
|
@ -85,18 +92,18 @@ Or come to say hello / share your feedback on [Discord](https://discord.gg/yTNZg
|
|||
|
||||
# Thanks to...
|
||||
This project depends on:
|
||||
- [CheerpX](https://labs.leaningtech.com/cheerpx), made by [Leaning Technologies](https://leaningtech.com) for x86 virtualization and Linux emulation
|
||||
- [CheerpX](https://cheerpx.io/), made by [Leaning Technologies](https://leaningtech.com/) for x86 virtualization and Linux emulation
|
||||
- xterm.js, [https://xtermjs.org/](https://xtermjs.org/), for providing the Web-based terminal emulator
|
||||
- [Tailscale](https://tailscale.com/), for the networking component
|
||||
- [lwIP](https://savannah.nongnu.org/projects/lwip/), for the TCP/IP stack, compiled for the Web via [Cheerp](https://github.com/leaningtech/cheerp-meta)
|
||||
- [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. 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:
|
||||
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).
|
||||
|
||||
`https://cheerpxdemos.leaningtech.com/publicdeploy/20230517_94/cx.js`
|
||||
The NPM package is updated on every release.
|
||||
|
||||
We strongly encourage users _not_ to use the latest build. Please directly use a specific build to avoid unexpected regressions. Since builds are immutable, if they work for you now they will keep working forever.
|
||||
Every build is immutable, if a specific version works well for you today, it will keep working forever.
|
||||
|
||||
# License
|
||||
|
||||
|
@ -104,6 +111,8 @@ 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 non-commercial uses. Downloading a CheerpX build for the purpose of hosting it elsewhere is not permitted.
|
||||
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 |
BIN
assets/social_2024.png
Normal file
After Width: | Height: | Size: 185 KiB |
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: 47 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
|
||||
};
|
|
@ -7,7 +7,7 @@
|
|||
**When all set:**
|
||||
- Log in with your Tailscale credentials.
|
||||
- Go back to the WebVM tab.
|
||||
- `Connect via Tailscale` should be replaced by your IP address.
|
||||
- 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
|
||||
|
||||
|
|
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
|
385
index.html
|
@ -1,385 +0,0 @@
|
|||
<!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&display=swap&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="./xterm/xterm-addon-web-links.js"></script>
|
||||
<script src="network.js"></script>
|
||||
<script defer data-domain="webvm.io" src="https://plausible.leaningtech.com/js/script.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 ❤️ 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;">🟢</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;">🟢</span>
|
||||
</div>
|
||||
<a id="loginLink" style="user-select: text ;text-decoration: none; height: 100%;">
|
||||
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
|
||||
<div style="position: relative;">
|
||||
<span style="cursor: pointer" id="networkStatus">Connect via Tailscale </span>
|
||||
<span style="cursor: pointer; position: absolute; right: 0px; visibility: hidden;" id="ipCopied">Copied! </span>
|
||||
</div>
|
||||
<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 = {
|
||||
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!] Your own WebVM with custom images via Dockerfile: |",
|
||||
"| |",
|
||||
"| " + underline + "https://leaningtech.com/mini-webvm-your-linux-box-from-dockerfile-via-wasm" + normal +" |",
|
||||
"| |",
|
||||
"| |",
|
||||
"| Join our Discord server for updates: |",
|
||||
"| |",
|
||||
"| " + underline + "https://discord.leaningtech.com" + 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 isCustom = window.location.hostname !== "webvm.io";
|
||||
const isSecureContext = window.isSecureContext;
|
||||
const text = [
|
||||
"",
|
||||
"",
|
||||
color + "CheerpX could not start" + normal,
|
||||
"",
|
||||
"CheerpX uses SharedArrayBuffer, which is not available right now.",
|
||||
"",
|
||||
!isSecureContext && " - This page is not in a secure context. Serve over HTTPS or WSS.",
|
||||
!isSecureContext && " " + underline + "https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts" + normal,
|
||||
isCustom && " - The document is not cross-origin isolated.",
|
||||
isCustom && " " + underline + "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements" + normal,
|
||||
" - Your browser might not support SharedArrayBuffer.",
|
||||
" Since 2022, all major browsers support this feature.",
|
||||
" " + underline + "https://caniuse.com/sharedarraybuffer" + normal,
|
||||
].filter(Boolean);
|
||||
|
||||
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);
|
||||
var linkAddon = new WebLinksAddon.WebLinksAddon();
|
||||
term.loadAddon(linkAddon);
|
||||
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);
|
||||
|
||||
function hddCallback(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}";
|
||||
}
|
||||
|
||||
//Actual CheerpX and bash specific logic
|
||||
async function runBash()
|
||||
{
|
||||
// cmd, cwd, args and env are replaced by the Github actions workflow.
|
||||
var cmd = CMD;
|
||||
var args = ARGS;
|
||||
var env = ENV;
|
||||
var cwd = CWD;
|
||||
var device_type = DEVICE_TYPE;
|
||||
// Reasonable defaults for local deployments
|
||||
// var cmd = "/bin/bash";
|
||||
// var args = ["--login"];
|
||||
// var env = ["HOME=/home/user", "TERM=xterm", "USER=user", "SHELL=/bin/bash", "EDITOR=vim", "LANG=en_US.UTF-8", "LC_ALL=C"];
|
||||
// var cwd = "/home/user";
|
||||
// var device_type = "bytes";
|
||||
const structure = {
|
||||
cmd: cmd,
|
||||
args: args,
|
||||
env: env,
|
||||
cwd: cwd
|
||||
}
|
||||
if (typeof SharedArrayBuffer === "undefined")
|
||||
{
|
||||
printOnTerm.printError(printOnTerm.getSharedArrayBufferMissingMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
let networkInterface = setupNetworkInterface();
|
||||
|
||||
async function runTest(cx)
|
||||
{
|
||||
var processCount = 0;
|
||||
function handleProcessCreated()
|
||||
{
|
||||
processCount++;
|
||||
plausible(`Process started: ${processCount}`);
|
||||
if(processCount == 5)
|
||||
{
|
||||
// Make sure no further event is reported
|
||||
cx.unregisterCallback("processCreated", handleProcessCreated);
|
||||
}
|
||||
}
|
||||
cx.registerCallback("processCreated", handleProcessCreated);
|
||||
cx.registerCallback("cpuActivity", cpuCallback);
|
||||
cx.registerCallback("diskActivity", hddCallback);
|
||||
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:structure.cwd, uid: 1000, gid: 1000};
|
||||
while (true)
|
||||
{
|
||||
await cxLogAndRun(cx, structure.cmd, structure.args, opts);
|
||||
}
|
||||
}
|
||||
function failCallback(err)
|
||||
{
|
||||
printOnTerm.printError(printOnTerm.getErrorMessage(err));
|
||||
}
|
||||
// The device url and type are replaced by Github Actions.
|
||||
var blockDevice;
|
||||
switch (device_type)
|
||||
{
|
||||
case "block":
|
||||
blockDevice = await CheerpX.CloudDevice.create(IMAGE_URL);
|
||||
break;
|
||||
case "bytes":
|
||||
blockDevice = await CheerpX.HttpBytesDevice.create(IMAGE_URL);
|
||||
break;
|
||||
case "split":
|
||||
blockDevice = await CheerpX.GitHubDevice.create(IMAGE_URL);
|
||||
break;
|
||||
default:
|
||||
console.log("Unrecognized device type");
|
||||
return;
|
||||
}
|
||||
var overlayDevice = await CheerpX.OverlayDevice.create(blockDevice, await CheerpX.IDBDevice.create("block1"));
|
||||
var webDevice = await CheerpX.WebDevice.create("");
|
||||
var dataDevice = await CheerpX.DataDevice.create();
|
||||
CheerpX.Linux.create({mounts:[{type:"ext2",dev:overlayDevice,path:"/"},{type:"tree",dev:webDevice,path:"/app"},{type:"tree",dev:dataDevice,path:"/data"},{type:"devs",path:"/dev"},{type:"proc",path:"/proc"}], networkInterface: networkInterface}).then(runTest, failCallback);
|
||||
}
|
||||
function initialMessage()
|
||||
{
|
||||
printOnTerm.printMessage(printOnTerm.getAsciiText());
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
|
92
network.js
|
@ -1,92 +0,0 @@
|
|||
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 ipCopiedElem = document.getElementById("ipCopied");
|
||||
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.innerText = "IP: "+ip;
|
||||
loginElem.title = "Right click to copy"
|
||||
const rmb_to_copy = (event) => {
|
||||
// To prevent the default contexmenu from showing up when right-clicking..
|
||||
event.preventDefault();
|
||||
// Copy the IP to the clipboard.
|
||||
window.navigator.clipboard.writeText(ip)
|
||||
.catch((msg) => { console.log("network.js: Copy ip to clipboard: Error: " + msg) });
|
||||
statusElem.style.visibility = "hidden";
|
||||
ipCopiedElem.style.visibility = "unset";
|
||||
setTimeout(() => {
|
||||
statusElem.style.visibility = "unset";
|
||||
ipCopiedElem.style.visibility = "hidden";
|
||||
}, 2000);
|
||||
};
|
||||
loginElem.addEventListener("contextmenu", rmb_to_copy);
|
||||
};
|
||||
loginElem.style.cursor = "pointer";
|
||||
loginElem.title = "Connect to Tailscale";
|
||||
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;
|
||||
statusElem.innerHTML = "Login URL ready...";
|
||||
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;
|
||||
}}
|
||||
},
|
||||
}
|
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: [],
|
||||
}
|
||||
|
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: '' }
|
||||
]
|
||||
})
|
||||
]
|
||||
});
|