Compare commits

..

171 commits

Author SHA1 Message Date
Alessandro Pignotti
239376dcef Bump links on the main WebVM landing page 2024-11-13 17:39:50 +01:00
Alessandro Pignotti
16714160aa Link back to WebVM 2.0 post 2024-11-13 12:34:38 +01:00
Alessandro Pignotti
52dcded70b Add example image 2024-11-12 18:00:11 +01:00
Alessandro Pignotti
fb4117d844 Bump social description 2024-11-12 09:35:16 +01:00
Alessandro Pignotti
d635622d6f Bump NPM deps
CheerpX -> 1.0.6
2024-11-11 16:15:48 +01:00
Alessandro Pignotti
74fbbc2209 Actually update the PDF 2024-11-11 16:07:52 +01:00
Alessandro Pignotti
3e42875cb9 Add sample PDF in documents 2024-11-11 12:43:50 +01:00
Alessandro Pignotti
8d473a0225 Clarify label for CPU load 2024-11-11 10:51:26 +01:00
Alessandro Pignotti
2222951621 Add customize cross-linking messages between main WebVM and Alpine 2024-11-09 23:04:01 +01:00
Alessandro Pignotti
aee895cabb Add the new background for alpine 2024-11-09 15:19:46 +01:00
Alessandro Pignotti
78be06afce Bump NPM deps
To get CheerpX 1.0.5
2024-11-09 15:12:06 +01:00
Alessandro Pignotti
ce162c9e8c Expose the documents directory in the user home 2024-11-09 15:10:32 +01:00
Alessandro Pignotti
d0e6d0e9ea Bump Alpine disk image 2024-11-09 15:10:05 +01:00
Alessandro Pignotti
9a5f77d4b7 Hide cursor on the display canvas
Xorg will show its own
2024-11-01 19:11:57 +01:00
Haseeb Qureshie
cdd095e776 Text change 2024-10-27 11:45:58 +01:00
Alessandro Pignotti
cd75685862 Bump NPM Deps
CheerpX -> 1.0.4
2024-10-23 13:54:15 +02:00
Alessandro Pignotti
b33e3a4356 Avoid reacting to spurious resize events
They are caused by soft-keyboard spawning
2024-10-23 10:56:38 +02:00
Alessandro Pignotti
004ea4f264 NPM: Bump deps
And especially CheerpX to 1.0.3
2024-10-22 21:07:12 +02:00
Alessandro Pignotti
00b5a3f42b Bump alpine image to a new one that supports screen resizing 2024-10-22 11:44:07 +02:00
Alessandro Pignotti
34f2a564db Nginx: Remove redirect to index.html for missing files
It was required for the SPA approach, but we migrated away from it
2024-10-21 23:00:18 +02:00
Alessandro Pignotti
46ffec65e2 Bump NPM deps
CX, in particular, to version 1.0.2
2024-10-21 14:08:52 +02:00
Alessandro Pignotti
5716055716 Bump WebVM version 2024-10-21 08:41:27 +02:00
Alessandro Pignotti
28f85baf59 Networking: Attempt a fix for a race condition during Tailscale setup 2024-10-20 11:00:35 +02:00
Alessandro Pignotti
59740754e8 Highlight icons during hover 2024-10-19 12:06:25 +02:00
Alessandro Pignotti
529f720ae1 Introduce blog post tab
Posts previews are automatically populated from social data from labs public URLs
2024-10-19 11:02:18 +02:00
Alessandro Pignotti
71715e5040 Remove legacy files 2024-10-18 20:07:01 +02:00
Alessandro Pignotti
46f21f3a12 Introduce preliminary support for screen resizing 2024-10-18 19:19:07 +02:00
Alessandro Pignotti
f3cf5750ab First attempt at responsive support for the display
NOTE: Resize is not yet supported
2024-10-18 19:09:29 +02:00
Alessandro Pignotti
a874b2a332 Use the regular styling for the disk reset button
Use the warning styling only for the confirmation
2024-10-18 17:56:07 +02:00
Alessandro Pignotti
310a9ce2e2 Make the canvas display fill up the screen 2024-10-18 13:02:11 +02:00
Alessandro Pignotti
7bf9990995 Prefer a fixed image size to avoid layout changes during loading 2024-10-18 12:46:41 +02:00
Alessandro Pignotti
e28cf214df Make activity feedback more compelling 2024-10-18 12:35:12 +02:00
Alessandro Pignotti
9b723dcb8a Implement the "Reset disk" button
This requires CheerpX 1.0.1
2024-10-18 12:12:56 +02:00
Alessandro Pignotti
8055466b7a Introduce the "Reset disk" button 2024-10-18 12:12:41 +02:00
Alessandro Pignotti
aa1935e389 Support icons for buttons 2024-10-18 12:12:14 +02:00
Alessandro Pignotti
51b63329ab Bump NPM deps
In particular bump CheerpX to 1.0.1
2024-10-18 11:43:57 +02:00
Alessandro Pignotti
0a5ceaf9ea Make button color configurable
In preparation for disk reset button
2024-10-18 11:41:50 +02:00
Alessandro Pignotti
236863a0b0 Rename buttonIcon to buttomImage
In preparation for supporting actual icons
2024-10-18 11:41:19 +02:00
Alessandro Pignotti
4fc2577819 First attempt at a responsive design, XTerm needs to coercion to behave 2024-10-18 09:18:48 +02:00
Alessandro Pignotti
88bea224d7 Remove a spurious reference to CORS 2024-10-17 20:38:33 +02:00
Alessandro Pignotti
3d56bc2182 Bump social preview 2024-10-17 14:13:08 +02:00
Alessandro Pignotti
56b40dbef8 Work around GH asset cache 2024-10-17 11:47:56 +02:00
Alessandro Pignotti
517a5997fb Bump README screenshot 2024-10-17 11:45:57 +02:00
Alessandro Pignotti
0f9381c00b Add WebVM logo art 2024-10-17 11:26:48 +02:00
Alessandro Pignotti
e01ffeb3db Restore the information tab
With a high-level overview of the moving parts of WebVM
2024-10-17 11:20:51 +02:00
Alessandro Pignotti
a26b523053 Rework Engine tab 2024-10-17 11:15:11 +02:00
Alessandro Pignotti
14621dbec2 Intro message copy/editing 2024-10-17 11:14:55 +02:00
Alessandro Pignotti
a3fc89faeb Make sure GH config can be safely uncommented to get something that works 2024-10-17 11:13:34 +02:00
Alessandro Pignotti
20533a8c35 Make the button more visibly clickable by adding a shadow 2024-10-17 11:07:46 +02:00
Alessandro Pignotti
32095987de Minor copy editing 2024-10-17 10:39:18 +02:00
Alessandro Pignotti
0069c378a7 Review README to match updated UI, versioning and licensing 2024-10-17 09:48:04 +02:00
Alessandro Pignotti
f64bebfe40 Add visual feedback about Tailscale exit nodes being present 2024-10-17 09:10:21 +02:00
Alessandro Pignotti
9d841e48a4 Fix tailscale regression 2024-10-16 17:56:35 +02:00
Alessandro Pignotti
2f4a22a659 Moderate GH star CTA 2024-10-16 16:55:22 +02:00
Alessandro Pignotti
26239de119 Add stars to GH panel 2024-10-16 16:45:11 +02:00
Alessandro Pignotti
98ab63f72c Favor font awesome to pure SVG 2024-10-16 16:44:47 +02:00
Alessandro Pignotti
43992f0864 Add Discords stats in the button 2024-10-16 15:51:42 +02:00
Alessandro Pignotti
e0e2fca2a0 Make sure plausible is loaded before code starts running 2024-10-16 13:42:59 +02:00
Alessandro Pignotti
86d4477e1c Catch unexpected errors and raise them to the console 2024-10-16 12:35:13 +02:00
Alessandro Pignotti
e4b9b50072 Support a env variable to select which CX build to load 2024-10-16 11:16:31 +02:00
Alessandro Pignotti
fcf626d03b Fix CPU load value
The new approach keep the last event before the time window limit to
correctly measure long running activity
2024-10-16 11:13:39 +02:00
Alessandro Pignotti
28afdc35ef Make sure the display canvas does not go over the side panel 2024-10-15 22:26:03 +02:00
Alessandro Pignotti
9858b64752 Add disk latency statistics 2024-10-15 22:24:26 +02:00
Alessandro Pignotti
cb1f2f7fc3 Compute the CPU load as a sliding window over 10 secs 2024-10-15 22:17:55 +02:00
Alessandro Pignotti
307669f7c4 Rename CpuTab to a more general Engine
And make the CPU load visible
2024-10-15 21:33:15 +02:00
Alessandro Pignotti
0f30d2273a Prefer relative paths one more time thanks to SSR 2024-10-15 19:16:30 +02:00
Alessandro Pignotti
b1956d3af8 GH: Do not deploy alpine 2024-10-15 19:14:04 +02:00
Alessandro Pignotti
98a0c2a47b Enable SSR 2024-10-15 17:23:42 +02:00
Alessandro Pignotti
d4db6f8e16 Give up on top-level inclusion of xterm and CheerpX
In preparation for SSR
2024-10-15 17:23:09 +02:00
Alessandro Pignotti
208cfa8e0d Network: Do not consider page parameters if not running in the browser
In preparation for SSR
2024-10-15 17:22:32 +02:00
Alessandro Pignotti
d28c611806 Alpine: Prevent duplicated reporting of display activation 2024-10-15 16:47:56 +02:00
Alessandro Pignotti
9962e2ce43 Make sure caches for the 2 demos do not override each other 2024-10-15 16:34:31 +02:00
Alessandro Pignotti
8adc03ac8f Alpine: Add analytics on display activation 2024-10-15 16:31:10 +02:00
Alessandro Pignotti
97fb17dfe5 GH: Fix pages deployment 2024-10-15 12:25:25 +02:00
Alessandro Pignotti
96805eca37 NPM: Bump deps to celebrate publication 2024-10-15 12:09:46 +02:00
Alessandro Pignotti
ba68b6fe02 CI: Bump to main WebVM domain 2024-10-15 12:06:19 +02:00
Alessandro Pignotti
930fc96242 Svelte: Restore analytics over started processes 2024-10-15 12:03:32 +02:00
Alessandro Pignotti
6ca6cf58a0 Svelte: Restore plausible integration 2024-10-15 12:02:51 +02:00
Alessandro Pignotti
201636aae7 GH++ 2024-10-15 11:28:06 +02:00
Alessandro Pignotti
1e407e1b45 GH++ 2024-10-15 11:25:21 +02:00
Alessandro Pignotti
1ecc59dcd5 GH++ 2024-10-15 11:22:03 +02:00
Alessandro Pignotti
12ed378cc2 GitHub: First attempt at GH pages deployment 2024-10-15 11:17:07 +02:00
Alessandro Pignotti
7172350071 Svelte: Fix resource location when using the subdirectory 2024-10-14 22:12:04 +02:00
Alessandro Pignotti
f3d0ab6fb3 Svelte: Remove alpine symlink, the built system is too fragile for them 2024-10-14 21:51:30 +02:00
Alessandro Pignotti
208fee9353 Svelte: Preserve symlinks 2024-10-14 21:46:09 +02:00
Alessandro Pignotti
0151a0fd16 CI: Install rsync 2024-10-14 21:41:52 +02:00
Alessandro Pignotti
1ee73b270a CI: Prefer rsync over scp to preserve symlinks 2024-10-14 21:38:12 +02:00
Alessandro Pignotti
5bcdeffb84 CI: Another attempt at scp 2024-10-14 20:47:20 +02:00
Alessandro Pignotti
2b01b09aab CI: Another attempt at scp 2024-10-14 20:42:16 +02:00
Alessandro Pignotti
9b9e8ffe44 CI: Argument order 2024-10-14 20:01:27 +02:00
Alessandro Pignotti
ecbea46e41 CI: Verbose copy 2024-10-14 19:59:30 +02:00
Alessandro Pignotti
b2ae6881d6 CI: Fix ssh host variable 2024-10-14 19:55:40 +02:00
Alessandro Pignotti
000f925216 CI: Use the node specific image 2024-10-14 19:36:18 +02:00
Alessandro Pignotti
ccbaf8749d Svelte: Remove rollup plugins after migration to sveltekit 2024-10-14 19:21:27 +02:00
Alessandro Pignotti
70c748a62a CI: Bump base image for nodejs version 2024-10-14 19:11:44 +02:00
Alessandro Pignotti
422d345904 CI: Run npm install 2024-10-14 19:10:03 +02:00
Alessandro Pignotti
e39a820f32 CI: Add GH host 2024-10-14 19:06:45 +02:00
Alessandro Pignotti
f6b931c273 Svelte: Prepopulate a directory structure to support alpine with the SPA approach 2024-10-14 19:02:50 +02:00
Alessandro Pignotti
7ab443c30c CI: Integrate circle for public webvm.io builds 2024-10-14 18:59:44 +02:00
Alessandro Pignotti
326cc40921 Svelte: Bump NPM package version 2024-10-14 18:54:09 +02:00
Alessandro Pignotti
60b8f103b6 Svelte: Introduce support for the alpine route 2024-10-14 16:40:28 +02:00
Alessandro Pignotti
c84a38ca02 Svelte: Extend the WebVM component to have graphical output 2024-10-14 16:39:57 +02:00
Alessandro Pignotti
f7fc244db4 Svelte: Expose more extensive configuration from the main WebVM component 2024-10-14 16:39:19 +02:00
Alessandro Pignotti
bb46f1340e Svelte: Generalize config file name
In preparation for adding fields about non-disk options
2024-10-14 15:43:08 +02:00
Alessandro Pignotti
d4fd5f1be5 Svelte: Pass a configuration object to the WebVM component
The config is loaded from files
2024-10-14 15:37:59 +02:00
Alessandro Pignotti
b1e87af6f2 Cleanup stale content in nginx.conf 2024-10-14 15:21:04 +02:00
Alessandro Pignotti
e2589d8179 Svelte: Extract a shared component in preparation for supporting multiple images 2024-10-14 15:04:40 +02:00
Alessandro Pignotti
0a9a044b27 Svelte: Restore printing CX init error messages to the visible console 2024-10-14 08:18:32 +02:00
Alessandro Pignotti
834566aa0e Svelte: Prototype GitHub tab 2024-10-14 08:09:00 +02:00
Alessandro Pignotti
5f2fe65fe7 Svelte: Prototype Discord tab 2024-10-14 07:53:51 +02:00
Alessandro Pignotti
f88f568f7e Svelte: Extract NetworkingTab button into a reusable component 2024-10-13 22:30:57 +02:00
Alessandro Pignotti
134a947547 Svelte: Restore welcome message
Compared to the side bar is much more visible
2024-10-13 22:14:58 +02:00
Alessandro Pignotti
854b93ec07 Svelte: Favor traditional absolute positioning for xterm and surrouding top-level components
xterm resizing interacts very poorly with flex
2024-10-13 22:14:23 +02:00
Alessandro Pignotti
fe7ab83fdd Svelte: Restore blinking icons 2024-10-13 22:14:23 +02:00
Alessandro Pignotti
12b3b3f89c Svelte: Protoype disk tab 2024-10-13 12:45:38 +02:00
Alessandro Pignotti
ff2486d6e2 Svelte: Minor highlighting of selected icon 2024-10-13 12:01:09 +02:00
Alessandro Pignotti
9de48becfa Svelte: Prototype CPU tab 2024-10-13 11:47:40 +02:00
Alessandro Pignotti
a43f439179 Remove svelte favicon 2024-10-13 11:47:25 +02:00
Alessandro Pignotti
f4648b08e6 Svelte: Restore support for Tailscale connection 2024-10-13 11:47:25 +02:00
Alessandro Pignotti
de34cb587e Add missing files for Sveltekit support 2024-10-13 11:21:50 +02:00
Alessandro Pignotti
113ef58d50 Initial prototype for Network sidebar 2024-10-12 16:44:03 +02:00
Alessandro Pignotti
96ddd4b2de Temporarily disable the information section 2024-10-11 21:56:19 +02:00
Alessandro Pignotti
fc64f9f987 Centralize handling of mouseleave to hide the side-panel 2024-10-11 08:17:52 +02:00
Alessandro Pignotti
f1ef46cda1 Extend Copyright since first release 2024-10-10 23:15:15 +02:00
Alessandro Pignotti
0d60f79c99 Convert to SvelteKit setup
With an SPA
2024-10-10 22:26:24 +02:00
Alessandro Pignotti
a21802e25e Minor restructure in preparation for SvelteKit 2024-10-10 16:21:29 +02:00
Alessandro Pignotti
98afe6dd00 New directory structure in preparation for SvelteKit migration 2024-10-10 16:06:47 +02:00
Alessandro Pignotti
ad7489d869 Disable source map generation in production mode 2024-10-06 09:05:24 +02:00
Alessandro Pignotti
ed52c12a83 Manually remove unused font-awesome classes 2024-10-06 09:03:13 +02:00
Alessandro Pignotti
a838ffc97a Minimize CSS in production 2024-10-05 23:21:44 +02:00
Alessandro Pignotti
df5bbfa0e1 Rollup build/dev configurations were inverted 2024-10-05 19:46:20 +02:00
Alessandro Pignotti
73f9e77a17 Basic support for running bash 2024-10-05 19:31:05 +02:00
Alessandro Pignotti
a7c4bc573c Avoid an extra pixel outside the terminal 2024-10-05 19:30:49 +02:00
Alessandro Pignotti
95c7a857b9 Late stage initialization for terminal support 2024-10-05 19:22:47 +02:00
Alessandro Pignotti
56257e6f69 Initialize the CheerpX engine 2024-10-05 19:11:05 +02:00
Alessandro Pignotti
f586c4bcf2 Initialize CheerpX disk backend for the main image 2024-10-05 19:10:57 +02:00
Alessandro Pignotti
74e18f2b38 Rework terminal initialization 2024-10-05 18:09:07 +02:00
Alessandro Pignotti
92edbb7b96 Initial support for configuring the disk via a self-contained config file 2024-10-05 18:09:07 +02:00
Alessandro Pignotti
274931dc12 Use CheerpX via the NPM module
This is a good solution for testing, but we will probably use the latest
CDN build later on
2024-10-05 18:09:07 +02:00
Alessandro Pignotti
4f13791f71 Use ES6 module output
For compatibility with top-level await in CX NPM package
2024-10-05 18:09:07 +02:00
Alessandro Pignotti
fdf57ef9b0 Copy over a bunch of HTML metadata 2024-10-05 18:09:07 +02:00
Alessandro Pignotti
978187d61b Introduce xterm.js 2024-10-05 18:09:07 +02:00
Alessandro Pignotti
5094953eb2 Unify handling of top and bottom sidebar icons 2024-10-05 18:09:07 +02:00
Alessandro Pignotti
05c234a528 Use Awesome font for sidebar symbols 2024-10-05 18:09:07 +02:00
Alessandro Pignotti
06df68fdea Add footer to all the possible sidebar contents 2024-10-05 18:09:07 +02:00
Alessandro Pignotti
53413d087b Bump title 2024-10-05 18:09:07 +02:00
Alessandro Pignotti
1760888da2 Hide the side bar when no icon is selected 2024-10-05 18:09:07 +02:00
Alessandro Pignotti
bffe17fd69 Display custom text for every icon 2024-10-05 18:09:07 +02:00
Alessandro Pignotti
f2fb54c29f Handle text color at the container level 2024-10-05 18:09:07 +02:00
Alessandro Pignotti
9a2dcb0c8f Initial support for the side panel 2024-10-05 18:09:07 +02:00
Alessandro Pignotti
20e7cdc5db Improve layout for basic icons 2024-10-05 18:09:07 +02:00
Alessandro Pignotti
43e10289c9 Preliminary support for sidebar icons 2024-10-05 18:09:07 +02:00
Alessandro Pignotti
1f16f35ef2 Stub SideBar 2024-10-05 18:09:07 +02:00
Alessandro Pignotti
cbcd67e884 Restructure in preparation for Sidebar integration 2024-10-05 18:09:07 +02:00
Alessandro Pignotti
5c78854b61 Prefer tailwind to plain CSS 2024-10-05 18:09:07 +02:00
Alessandro Pignotti
f3fbf4a29f Import tailwind to achieve full visual consistency with labs 2024-10-05 18:09:07 +02:00
Alessandro Pignotti
460e3a2726 Import the same font used by labs, for consistency 2024-10-05 18:09:07 +02:00
Alessandro Pignotti
891f15dddf Basic CSS cleanup 2024-10-05 18:09:07 +02:00
Alessandro Pignotti
0a5a6ef0a4 Import labs to use the shared navigation bar component 2024-10-05 18:09:07 +02:00
Alessandro Pignotti
d59c6c069b Basic npm setup for svelte 2024-10-05 18:09:07 +02:00
Alessandro Pignotti
e2d87483c9 Temporarily switch the Discord link with the hackathon notice 2024-10-05 17:22:14 +02:00
Alessandro Pignotti
cb1397e472 Use the new CX hosting 2024-10-02 10:25:11 +02:00
Yuri Iozzelli
4b84396f83 print an error on the terminal if the HttpBytesDevice fails to initialize 2024-10-01 15:48:42 +02:00
Alessandro Pignotti
c6ff3283d6 Add analytics even on WSS failures in the wild 2024-10-01 11:42:01 +02:00
Alessandro Pignotti
a45e2bd379 Use plain HTTP as fallback when WSS fails 2024-10-01 11:41:13 +02:00
Jules Saarikoski
84ba14ef7c Update tree device type to dir 2024-09-30 13:39:45 +02:00
Alessandro Pignotti
fcb3e6307f Alpine: Adapt to new APIs and introduce a protocol parameter to test new WS mode 2024-09-27 16:46:03 +02:00
Alessandro Pignotti
65c0fbf448 Cleanup block device terminology for new API 2024-09-27 08:56:54 +02:00
Jules Saarikoski
b6f5736784 Update to new API 2024-09-27 08:50:15 +02:00
Alessandro Pignotti
91b5d5462f Alpine: First version 2024-09-22 18:24:57 +02:00
Alessandro Pignotti
878effcfb1 Duplicate main WebVM page in preparation for Alpine demo 2024-09-22 18:24:57 +02:00
50 changed files with 4264 additions and 546 deletions

42
.circleci/config.yml Normal file
View 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

View file

@ -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
View file

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

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

1
assets/cheerpx.svg Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

BIN
assets/webvm_hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

23
config_github_terminal.js Normal file
View 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
View 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
View 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
};

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 KiB

Binary file not shown.

5
documents/Welcome.txt Normal file
View 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
View file

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

View file

@ -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&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="./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 &#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="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>

View file

@ -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();
};
}
}

View file

@ -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

File diff suppressed because it is too large Load diff

31
package.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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>

View 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>

View 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>

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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 }

View 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
View file

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

16
src/routes/+page.svelte Normal file
View 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>

View file

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

View 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
View 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
View 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
View 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: '' }
]
})
]
});