Compare commits

...
Sign in to create a new pull request.

267 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
Alex Bates
2ff80eb759
add discord cta 2024-07-23 17:12:43 +01:00
Alessandro Pignotti
bb21cf7077 Mount proc, CheerpX now has partial support for it 2024-07-22 10:43:47 +02:00
Alessandro Pignotti
9ad64fbe9e Get rid of Google Tag Manager
We have migrated to plausible long ago
2024-07-22 10:43:47 +02:00
Vaibhav Sagar
699d582a75 Update README.md 2024-07-12 16:11:02 +02:00
Alessandro Pignotti
e2085f7210 Fix a typo in error handling 2024-07-12 08:40:21 +02:00
Alessandro Pignotti
9ac854268c Remove stale method 2024-07-08 11:52:27 +02:00
Alessandro Pignotti
aeb387a92d Bump xterm version
This seems to resolve a long standing performance issue on mobile during terminal resizing
2024-07-08 11:51:49 +02:00
Alessandro Pignotti
9f3308e422 Add telemetry about the number of processes started in a session 2024-07-04 16:53:50 +02:00
Alessandro Pignotti
9b07e72065 Migrate activity monitoring to the new events API 2024-07-04 16:53:20 +02:00
Alex Bates
36db7dd37d don't use artifacts v4
https://github.blog/changelog/2023-12-14-github-actions-artifacts-v4-is-now-generally-available
2023-12-20 14:33:31 +00:00
Alex Bates
ef5e3361cd update upload-pages-artifact and deploy-pages actions 2023-12-20 14:22:44 +00:00
Alessandro Pignotti
e81a1ef3a3 Attempt to fix GH flow 2023-12-19 21:13:17 +01:00
Alex Bates
87a0471225
improve SharedArrayBuffer error message (fix #91) 2023-12-15 15:44:11 +00:00
Alex Bates
e74d20a60a
add Plausible analytics script 2023-09-28 13:16:50 +01:00
Alex Bates
e351ccdfa5
add shields 2023-08-17 09:45:57 +01:00
phaleth
6c584c5d9d Fix typo in README.md 2023-06-18 14:34:09 +02:00
Alessandro Pignotti
fc3861b4f0 UI: Simplified approach to IP copying support 2023-05-30 12:35:59 +02:00
zinobias
5add2e167d index.html: Tooltips for tailsccale login & copy w/ right-click.
IP is now selectable, no longer draggable. Button still draggable.
Right clicking the IP/icon will copy the IP to the clipboard and print a
tooltip underneath the cursor "Copied to clipboard".
2023-05-30 12:22:05 +02:00
zinobias
e834e5e316 Workflow: Guard clause has its own job, error handling for poor pages
configuration.
2023-05-23 14:35:15 +02:00
Alessandro Pignotti
cfac9d0310 Network: Be slightly more verbose 2023-05-23 09:10:27 +02:00
Alessandro Pignotti
548779b08d Simplify using the local default configuration 2023-05-23 09:10:03 +02:00
Alessandro Pignotti
3190b35d18 Bump README for local deployment 2023-05-22 17:19:38 +02:00
Alessandro Pignotti
b16dedd9de Restore reasonable defaults for local deployment 2023-05-22 17:18:28 +02:00
Alessandro Pignotti
f5c40e4723 README: Add Python REPL example 2023-05-22 17:07:40 +02:00
zinobias
5a3e29c5d6 Gh workflow now extracts cmd / env / args & cwd from the Docker container 2023-05-22 16:36:50 +02:00
zinobias
8190cb971d Dockerfiles: debian_mini, set workdir and Env for following commits. 2023-05-22 16:36:50 +02:00
Alessandro Pignotti
200f23fc18 Add new article to README 2023-05-22 15:11:27 +02:00
Alessandro Pignotti
e62d875ec6 Bump featured article 2023-05-22 15:08:34 +02:00
Alessandro Pignotti
2edcb7e7fd Bump README 2023-05-22 14:00:10 +02:00
Alessandro Pignotti
3521143cd3 Bump asset 2023-05-22 13:58:27 +02:00
elisabeth
8329838383 Set the root and user passwords to password in the Dockerfiles 2023-05-22 12:29:49 +02:00
Alessandro Pignotti
78a0c4de62 Bump README 2023-05-22 10:38:33 +02:00
Alessandro Pignotti
5ce43e7e39 Bump README 2023-05-22 10:30:43 +02:00
Alessandro Pignotti
4a56f44b48 Bump README 2023-05-22 10:29:35 +02:00
Alessandro Pignotti
be770a0fc4 Bump README 2023-05-22 10:17:15 +02:00
Alessandro Pignotti
e5984ccffe Bump README 2023-05-22 10:16:26 +02:00
Alessandro Pignotti
66f26862ac Bump README 2023-05-22 10:15:29 +02:00
Alessandro Pignotti
eac45f58f4 Bump README 2023-05-22 10:13:34 +02:00
Alessandro Pignotti
567abd352e Add Apache 2.0 license 2023-05-22 10:03:07 +02:00
Alessandro Pignotti
2e355e2d04 Copy editing 2023-05-19 12:10:46 +02:00
Alessandro Pignotti
feaad0842c Copy editing 2023-05-19 11:48:51 +02:00
Alessandro Pignotti
8e8d77bc6e Simplify workflow configuration 2023-05-19 11:04:40 +02:00
Alessandro Pignotti
4f490dc153 Copy editing 2023-05-19 11:02:58 +02:00
Alessandro Pignotti
145a6cfb7e Direct link to #webvm Discord channel 2023-05-19 10:24:14 +02:00
Alessandro Pignotti
55f5881e60 Copy editing 2023-05-19 10:21:23 +02:00
Alessandro Pignotti
64c560396d Copy editing 2023-05-19 10:17:46 +02:00
Alessandro Pignotti
4c472f1af2 New split endpoint does not support for slow first load anymore 2023-05-17 23:06:45 +02:00
Alessandro Pignotti
f652d3763e Workflow: Use power-of-two sizes for the image 2023-05-17 22:47:07 +02:00
Alessandro Pignotti
9b4dcf8a2d Use the new "split" device
Pre-process the ext2 image into chunks to work round the poor
performance of GitHub pages for large files
2023-05-17 22:01:53 +02:00
zinobias
941e86919d updated /assets/welcome_to_WebVM_slim.png 2023-05-17 19:12:19 +02:00
zinobias
6f142efb42 readme: Clarified running the action can take a couple of minutes. 2023-05-17 19:12:19 +02:00
zinobias
174e9c4738 Readme: Removed the self-hosting webvm section, seems obsolete. 2023-05-17 19:12:19 +02:00
zinobias
537f1fa805 Actions: Removed potential priming of the image, doesnt seem to work. 2023-05-17 19:12:19 +02:00
Alessandro Pignotti
fd2918b6cf Avoid leaving the page on ribbon click 2023-05-17 17:13:21 +02:00
Alessandro Pignotti
87babe55de Minor copy-editing 2023-05-17 17:12:14 +02:00
Alessandro Pignotti
791d9e66bf UI: Minor spacing adjustment 2023-05-17 17:04:04 +02:00
zinobias
245ac6df4f Added cowsay and netcat-openbsd to the debian_mini Dockerfile. 2023-05-17 17:00:42 +02:00
zinobias
0cc892e6e6 Updated /assets/welcome_to_WebVM_slim and adjusted the scaling. 2023-05-17 17:00:42 +02:00
zinobias
28a7309328 Moved networking instructions from README.md. Some refactor. 2023-05-17 17:00:42 +02:00
zinobias
fb184d75fd Adjusted README.md to reflect the recent changes to WebVM 2023-05-17 17:00:42 +02:00
zinobias
9910ed2928 Added Gh action gif & png to assets 2023-05-17 17:00:42 +02:00
Alessandro Pignotti
2fd6e0a078 Add a caveat about first loading 2023-05-17 16:32:56 +02:00
Alessandro Pignotti
c3891eb870 UI: Add GH ribbon 2023-05-17 12:30:30 +02:00
Alessandro Pignotti
59fe7fbe70 UI: Minor adjustment for logos 2023-05-17 11:23:21 +02:00
Alessandro Pignotti
5129627b26 Actions: Tun interface is now part of the CX deployment 2023-05-17 09:49:40 +02:00
zinobias
3e91dbed43 actions: fix to priming the gh image. 2023-05-17 08:22:19 +02:00
Yuri Iozzelli
73d3ab20a8 Use tailscale provided by CheerpX directly, instead of vendoring it 2023-05-16 17:55:29 +02:00
zinobias
09cf54e003 Added netcat-openbsd to the debian_large dockerfile 2023-05-16 17:16:44 +02:00
Alessandro Pignotti
151a4eda64 Deprecate the tinycore demo 2023-05-16 16:17:42 +02:00
Alessandro Pignotti
8ae7fb95b3 Actions: Prime the image on GH pages 2023-05-16 14:13:05 +02:00
zinobias
5f08aaf361 All the Dockerfiles now share one directory. 2023-05-16 14:09:51 +02:00
zinobias
8c6537e2a6 Refactor make_ext2_workflow_inputs 2023-05-16 14:09:51 +02:00
zinobias
eed08ca1d7 webvm deployment workflow now displays final url in a cleaner way. 2023-05-16 14:09:51 +02:00
zinobias
945d648035 Adjustment to workflow to set dns nameservers and preserve resolv.conf. 2023-05-16 14:09:51 +02:00
zinobias
556cdad01c Added 1900mb debian image based on webvm.io 2023-05-16 14:09:51 +02:00
zinobias
6c453a852e Curl/less added to base deb dockerfile. Removed xauth and udev. 2023-05-16 14:09:51 +02:00
Alessandro Pignotti
8b3be6839e Make sure index.html remains readable after variable replacement 2023-05-16 11:58:53 +02:00
Alessandro Pignotti
1233d181f8 Actions: Remove CX_VERSION replacement 2023-05-16 11:57:52 +02:00
Alessandro Pignotti
e9f22048c5 Use the latest CX build at any time 2023-05-16 11:57:17 +02:00
Alessandro Pignotti
f086827963 GHActions: Bump the default CX version 2023-05-15 20:36:45 +02:00
Alessandro Pignotti
15be98fd7b Prefer orange to yellow for activity lights 2023-05-15 20:35:44 +02:00
Alessandro Pignotti
0f1151af71 Use the new CX activity callbacks to provide feedback into the machine state 2023-05-15 19:39:57 +02:00
Alessandro Pignotti
dc3b736901 CI: Reduce default image size to partially compensate for poor performance of GitHub pages on first access 2023-05-15 18:55:09 +02:00
Alessandro Pignotti
2047ae83e1 Fix links 2023-05-09 11:45:06 +02:00
Alessandro Pignotti
ac2897bd6f Fix the viewport for mobile 2023-05-08 17:31:40 +02:00
Alessandro Pignotti
c95181f909 Bump social preview images 2023-05-08 16:58:46 +02:00
Alessandro Pignotti
cbb27671c7 Minor copy-editing 2023-05-08 16:23:08 +02:00
Alessandro Pignotti
d4a9f76091 Add assets 2023-05-08 16:10:39 +02:00
Alessandro Pignotti
a47359325a UI rework 2023-05-08 15:39:26 +02:00
zinobias
395ad534b1 Added a replaceable variable for the device type in index.html 2023-05-08 15:37:28 +02:00
zinobias
ddb71b79a9 Gh actions workflow to build/deploy Dockerfile based ext2 image to
gh-pages. Or upload it as a release.
2023-05-04 23:53:06 +02:00
zinobias
e502dc50f3 Refactor index.html to work with new Github Actions workflow 2023-05-04 23:53:06 +02:00
zinobias
1385babdfb Added luajit interp lines to the other 2 lua examples. 2023-05-04 23:53:06 +02:00
zinobias
77a2622111 dockerfiles directory/files. preliminary commit gh action. 2023-05-04 23:53:06 +02:00
zinobias
783f271d4c serviceworker.js: Adjusted a comment in, to clarify the ifstatement in doRegister(). 2023-04-18 13:53:29 +02:00
zinobias
d871a7867b seviceworker.js: Added try / catch around reload call in doRegister to log in case of error 2023-04-18 13:53:29 +02:00
73 changed files with 4885 additions and 2856 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

245
.github/workflows/deploy.yml vendored Normal file
View file

@ -0,0 +1,245 @@
name: Deploy
# Define when the workflow should run
on:
# Allow manual triggering of the workflow from the Actions tab
workflow_dispatch:
# Allow inputs to be passed when manually triggering the workflow from the Actions tab
inputs:
DOCKERFILE_PATH:
type: string
description: 'Path to the Dockerfile'
required: true
default: 'dockerfiles/debian_mini'
IMAGE_SIZE:
type: string
description: 'Image size, 950M max'
required: true
default: '600M'
DEPLOY_TO_GITHUB_PAGES:
type: boolean
description: 'Deploy to Github pages'
required: true
default: true
GITHUB_RELEASE:
type: boolean
description: 'Upload GitHub release'
required: true
default: false
jobs:
guard_clause:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }} # As required by the GitHub-CLI
permissions:
actions: 'write' # Required in order to terminate the workflow run.
steps:
- uses: actions/checkout@v3
# Guard clause that cancels the workflow in case of an invalid DOCKERFILE_PATH and/or incorrectly configured Github Pages.
# The main reason for choosing this workaround for aborting the workflow is the fact that it does not display the workflow as successful, which can set false expectations.
- name: DOCKERFILE_PATH.
shell: bash
run: |
# We check whether the Dockerfile_path is valid.
if [ ! -f ${{ github.event.inputs.DOCKERFILE_PATH }} ]; then
echo "::error title=Invalid Dockerfile path::No file found at ${{ github.event.inputs.DOCKERFILE_PATH }}"
echo "terminate=true" >> $GITHUB_ENV
fi
- name: Github Pages config guard clause
if: ${{ github.event.inputs.DEPLOY_TO_GITHUB_PAGES == 'true' }}
run: |
# We use the Github Rest api to get information regarding pages for the Github Repository and store it into a temporary file named "pages_response".
set +e
gh api \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
/repos/${{ github.repository_owner }}/$(basename ${{ github.repository }})/pages > pages_response
# We make sure Github Pages has been enabled for this repository.
if [ "$?" -ne 0 ]; then
echo "::error title=Potential pages configuration error.::Please make sure you have enabled Github pages for the ${{ github.repository }} repository. If already enabled then Github pages might be down"
echo "terminate=true" >> $GITHUB_ENV
fi
set -e
# We make sure the Github pages build & deployment source is set to "workflow" (Github Actions). Instead of a "legacy" (branch).
if [[ "$(jq --compact-output --raw-output .build_type pages_response)" != "workflow" ]]; then
echo "Undefined behaviour, Make sure the Github Pages source is correctly configured in the Github Pages settings."
echo "::error title=Pages configuration error.::Please make sure you have correctly picked \"Github Actions\" as the build and deployment source for the Github Pages."
echo "terminate=true" >> $GITHUB_ENV
fi
rm pages_response
- name: Terminate run if error occurred.
run: |
if [[ $terminate == "true" ]]; then
gh run cancel ${{ github.run_id }}
gh run watch ${{ github.run_id }}
fi
build:
needs: guard_clause # Dependency
runs-on: ubuntu-latest # Image to run the worker on.
env:
TAG: "ext2-webvm-base-image" # Tag of docker image.
IMAGE_SIZE: '${{ github.event.inputs.IMAGE_SIZE }}'
DEPLOY_DIR: /webvm_deploy/ # Path to directory where we host the final image from.
permissions: # Permissions to grant the GITHUB_TOKEN.
contents: write # Required permission to make a github release.
steps:
# Checks-out our repository under $GITHUB_WORKSPACE, so our job can access it
- uses: actions/checkout@v3
# Setting the IMAGE_NAME variable in GITHUB_ENV to <Dockerfile name>_<date>_<run_id>.ext2.
- name: Generate the image_name.
id: image_name_gen
run: |
echo "IMAGE_NAME=$(basename ${{ github.event.inputs.DOCKERFILE_PATH }})_$(date +%Y%m%d)_${{ github.run_id }}.ext2" >> $GITHUB_ENV
# Create directory to host the image from.
- run: sudo mkdir -p $DEPLOY_DIR
# Build the i386 Dockerfile image.
- run: docker build . --tag $TAG --file ${{ github.event.inputs.DOCKERFILE_PATH }} --platform=i386
# Run the docker image so that we can export the container.
# Run the Docker container with the Google Public DNS nameservers: 8.8.8.8, 8.8.4.4
- run: |
docker run --dns 8.8.8.8 --dns 8.8.4.4 -d $TAG
echo "CONTAINER_ID=$(sudo docker ps -aq)" >> $GITHUB_ENV
# We extract the CMD, we first need to figure whether the Dockerfile uses CMD or an Entrypoint.
- name: Extracting CMD / Entrypoint and args
shell: bash
run: |
cmd=$(sudo docker inspect --format='{{json .Config.Cmd}}' $CONTAINER_ID)
entrypoint=$(sudo docker inspect --format='{{json .Config.Entrypoint}}' $CONTAINER_ID)
if [[ $entrypoint != "null" && $cmd != "null" ]]; then
echo "CMD=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Entrypoint' )" >> $GITHUB_ENV
echo "ARGS=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Cmd' )" >> $GITHUB_ENV
elif [[ $cmd != "null" ]]; then
echo "CMD=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Cmd[:1]' )" >> $GITHUB_ENV
echo "ARGS=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Cmd[1:]' )" >> $GITHUB_ENV
else
echo "CMD=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Entrypoint[:1]' )" >> $GITHUB_ENV
echo "ARGS=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Entrypoint[1:]' )" >> $GITHUB_ENV
fi
# We extract the ENV, CMD/Entrypoint and cwd from the Docker container with docker inspect.
- name: Extracting env, args and cwd.
shell: bash
run: |
echo "ENV=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Env' )" >> $GITHUB_ENV
echo "CWD=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.WorkingDir' )" >> $GITHUB_ENV
# We create and mount the base ext2 image to extract the Docker container's filesystem its contents into.
- name: Create ext2 image.
run: |
# Preallocate space for the ext2 image
sudo fallocate -l $IMAGE_SIZE ${IMAGE_NAME}
# Format to ext2 linux kernel revision 0
sudo mkfs.ext2 -r 0 ${IMAGE_NAME}
# Mount the ext2 image to modify it
sudo mount -o loop -t ext2 ${IMAGE_NAME} /mnt/
# We opt for 'docker cp --archive' over 'docker save' since our focus is solely on the end product rather than individual layers and metadata.
# However, it's important to note that despite being specified in the documentation, the '--archive' flag does not currently preserve uid/gid information when copying files from the container to the host machine.
# Another compelling reason to use 'docker cp' is that it preserves resolv.conf.
- name: Export and unpack container filesystem contents into mounted ext2 image.
run: |
sudo docker cp -a ${CONTAINER_ID}:/ /mnt/
sudo umount /mnt/
# Result is an ext2 image for webvm.
# The .txt suffix enabled HTTP compression for free
- name: Generate image split chunks and .meta file
run: |
sudo split ${{ env.IMAGE_NAME }} ${{ env.DEPLOY_DIR }}/${{ env.IMAGE_NAME }}.c -a 6 -b 128k -x --additional-suffix=.txt
sudo bash -c "stat -c%s ${{ env.IMAGE_NAME }} > ${{ env.DEPLOY_DIR }}/${{ env.IMAGE_NAME }}.meta"
# This step updates the default config_github_terminal.js file by performing the following actions:
# 1. Replaces all occurrences of IMAGE_URL with the URL to the image.
# 2. Replace CMD with the Dockerfile entry command.
# 3. Replace args with the Dockerfile CMD / Entrypoint args.
# 4. Replace ENV with the container's environment values.
# 5. Replace CWD with the container's current working directory.
- name: Adjust config_github_terminal.js
run: |
sed -i 's#IMAGE_URL#"${{ env.IMAGE_NAME }}"#g' config_github_terminal.js
sed -i 's#CMD#${{ env.CMD }}#g' config_github_terminal.js
sed -i 's#ARGS#${{ env.ARGS }}#g' config_github_terminal.js
sed -i 's#ENV#${{ env.ENV }}#g' config_github_terminal.js
sed -i 's#CWD#${{ env.CWD }}#g' config_github_terminal.js
- name: Build NPM package
run: |
npm install
WEBVM_MODE=github npm run build
# Move required files for gh-pages deployment to the deployment directory $DEPLOY_DIR.
- name: Copy build
run: |
rm build/alpine.html
sudo mv build/* $DEPLOY_DIR/
# We generate index.list files for our httpfs to function properly.
- name: make index.list
shell: bash
run: |
find $DEPLOY_DIR -type d | while read -r dir;
do
index_list="$dir/index.list";
sudo rm -f "$index_list";
sudo ls "$dir" | sudo tee "$index_list" > /dev/null;
sudo chmod +rw "$index_list";
sudo echo "created $index_list";
done
# Create a gh-pages artifact in order to deploy to gh-pages.
- name: Upload GitHub Pages artifact
uses: actions/upload-pages-artifact@v2
with:
# Path of the directory containing the static assets for our gh pages deployment.
path: ${{ env.DEPLOY_DIR }} # optional, default is _site/
- name: github release # To upload our final ext2 image as a github release.
if: ${{ github.event.inputs.GITHUB_RELEASE == 'true' }}
uses: softprops/action-gh-release@v0.1.15
with:
target_commitish: ${{ github.sha }} # Last commit on the GITHUB_REF branch or tag
tag_name: ext2_image
fail_on_unmatched_files: 'true' # Fail in case of no matches with the file(s) glob(s).
files: | # Assets to upload as release.
${{ env.IMAGE_NAME }}
deploy_to_github_pages: # Job that deploys the github-pages artifact to github-pages.
if: ${{ github.event.inputs.DEPLOY_TO_GITHUB_PAGES == 'true' }}
needs: build
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
permissions:
pages: write # to deploy to Pages
id-token: write # to verify the deployment originates from an appropriate source
runs-on: ubuntu-latest
steps:
# Deployment to github pages
- name: Deploy GitHub Pages site
id: deployment
uses: actions/deploy-pages@v3

1
.npmrc Normal file
View file

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

202
LICENSE.txt Normal file
View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

126
README.md
View file

@ -1,70 +1,118 @@
# WebVM
This repository hosts the source code of for [https://webvm.io](https://webvm.io), a Linux virtual machine that runs in your browser.
[![Discord server](https://img.shields.io/discord/988743885121548329?color=%235865F2&logo=discord&logoColor=%23fff)](https://discord.gg/yWRr2YnD9c)
[![Issues](https://img.shields.io/github/issues/leaningtech/webvm)](https://github.com/leaningtech/webvm/issues)
<img src="assets/welcome_to_WebVM_slim.png" width="70%">
This repository hosts the source code for [https://webvm.io](https://webvm.io), a Linux virtual machine that runs in your browser.
<img src="/assets/welcome_to_WebVM_2024.png" width="70%">
WebVM is a server-less virtual environment running fully client-side in HTML5/WebAssembly. It's designed to be Linux ABI-compatible. It runs an unmodified Debian distribution including many native development toolchains.
WebVM is powered by the CheerpX virtualization engine, and enables safe, sandboxed client-side execution of x86 binaries on any browser. CheerpX includes an x86-to-WebAssembly JIT compiler, a virtual block-based file system, and a Linux syscall emulator.
# How to: general usage
# Enable networking
- go to [https://webvm.io](https://webvm.io)
- use the provided terminal environment
- have fun!
Modern browsers do not provide APIs to directly use TCP or UDP. WebVM provides networking support by integrating with Tailscale, a VPN network that supports WebSockets as a transport layer.
# How to: enable networking
- Open the "Networking" panel from the side-bar
- Click "Connect to Tailscale" from the panel
- Log in to Tailscale (create an account if you don't have one)
- Click "Connect" when prompted by Tailscale
- If you are unfamiliar with Tailscale or would like additional information see [WebVM and Tailscale](/docs/Tailscale.md).
- go to [https://webvm.io](https://webvm.io)
- click "Tailscale Login" in the top right corner
- log in to Tailscale (create an accout if you don't have one)
- if you want to access the public internet, you need an Exit Node. See [here](https://tailscale.com/kb/1103/exit-nodes/) for how to set one up. If you just want to access a machine in your Tailscale Network, you don't need it
- depending on your network speed, you may need to wait a few moments for the Tailscale Wasm module to be downloaded
- log in with your Tailscale credentials
- go back to the WebVM tab. You will see your IP address in the top right
- start firing network requests!
# Fork, deploy, customize
# How to: login to Tailscale with an Auth key
<img src="/assets/fork_deploy_instructions.gif" alt="deploy_instructions_gif" width="90%">
- Add `#authKey=<your-key>` at the end of the URL
- Done. You don't need to manually log in anymore
- Fork the repository.
- Enable Github pages in settings.
- Click on `Settings`.
- Go to the `Pages` section.
- Select `Github Actions` as the source.
- If you are using a custom domain, ensure `Enforce HTTPS` is enabled.
- Run the workflow.
- Click on `Actions`.
- Accept the prompt. This is required only once to enable Actions for your fork.
- Click on the workflow named `Deploy`.
- Click `Run workflow` and then once more `Run workflow` in the menu.
- After a few seconds a new `Deploy` workflow will start, click on it to see details.
- After the workflow completes, which takes a few minutes, it will show the URL below the `deploy_to_github_pages` job.
It is recommended to use an ephemeral key.
<img src="/assets/result.png" width="70%" >
# How to: login to a self-hosted Tailscale network (Headscale)
You can now customize `dockerfiles/debian_mini` to suit your needs, or make a new Dockerfile from scratch. Use the `Path to Dockerfile` workflow parameter to select it.
- Add `#controlUrl=<your-control-url>` at the end of the URL
- You can combine this option with `authKey` with a `&`: `#controlUrl=<url>&authKey=<key>`
# Local deployment
# How to host WebVM locally
From a local `git clone`
- Replace `CX_VERSION` in index.html and tinycore.html with a valid version of CheerpX. The latest version can be found at [https://webvm.io](https://webvm.io)
- Run nginx -p . -c nginx.conf in the root of the WebVM directory. WebVM can then be found at `http://localhost:8081`
- Download the `debian_mini` Ext2 image from [https://github.com/leaningtech/webvm/releases/](https://github.com/leaningtech/webvm/releases/)
- You can also build your own by selecting the "Upload GitHub release" workflow option
- Place the image in the repository root folder
- Edit `config_github_terminal.js`
- Uncomment the default values for `CMD`, `ARGS`, `ENV` and `CWD`
- Replace `IMAGE_URL` with the URL (absolute or relative) for the Ext2 image. For example `"/debian_mini_20230519_5022088024.ext2"`
- Build WebVM using `npm`, output will be placed in the `build` directory
- `npm install`
- `npm run build`
- Start NGINX, it automatically points to the `build` directory just created
- `nginx -p . -c nginx.conf`
- Visit `http://127.0.0.1:8081` and enjoy your local WebVM
# Example customization: Python3 REPL
The `Deploy` workflow takes into account the `CMD` specified in the Dockerfile. To build a REPL you can simply apply this patch and deploy.
```diff
diff --git a/dockerfiles/debian_mini b/dockerfiles/debian_mini
index 2878332..1f3103a 100644
--- a/dockerfiles/debian_mini
+++ b/dockerfiles/debian_mini
@@ -15,4 +15,4 @@ WORKDIR /home/user/
# We set env, as this gets extracted by Webvm. This is optional.
ENV HOME="/home/user" TERM="xterm" USER="user" SHELL="/bin/bash" EDITOR="vim" LANG="en_US.UTF-8" LC_ALL="C"
RUN echo 'root:password' | chpasswd
-CMD [ "/bin/bash" ]
+CMD [ "/usr/bin/python3" ]
```
# Bugs and Issues
Please use [Issues](https://github.com/leaningtech/webvm/issues) to report any bug.
Or come to say hello / share your feedback on [Discord](https://discord.leaningtech.com).
# Browsers support
WebVM and CheerpX are compatible with any browser, both Desktop and Mobile, provided support for [SAB](https://caniuse.com/sharedarraybuffer), [IndexedDB](https://caniuse.com/indexeddb), and the device having enough memory.
Or come to say hello / share your feedback on [Discord](https://discord.gg/yTNZgySKGa).
# More links
- [Do: WebVM](https://webvm.io)
- [Read: WebVM](https://leaningtech.com/webvm-server-less-x86-virtual-machines-in-the-browser/)
- [Read: WebVM + Tailscale networking](https://leaningtech.com/webvm-virtual-machine-with-networking-via-tailscale/)
- [Learn: WebVM](https://leaningtech.com/webvm)
- [Watch: WebVM at GitNation](https://www.youtube.com/watch?v=VqrbVycTXmw)
- [WebVM: server-less x86 virtual machines in the browser](https://leaningtech.com/webvm-server-less-x86-virtual-machines-in-the-browser/)
- [WebVM: Linux Virtualization in WebAssembly with Full Networking via Tailscale](https://leaningtech.com/webvm-virtual-machine-with-networking-via-tailscale/)
- [Mini.WebVM: Your own Linux box from Dockerfile, virtualized in the browser via WebAssembly](https://leaningtech.com/mini-webvm-your-linux-box-from-dockerfile-via-wasm/)
- Reference GitHub Pages deployment: [Mini.WebVM](https://mini.webvm.io)
- [Crafting the Impossible: X86 Virtualization in the Browser with WebAssembly](https://www.youtube.com/watch?v=VqrbVycTXmw) Talk at JsNation 2022
# Thanks to...
This project depends on:
- CheerpX, made by [Leaning Technologies](https://leaningtech.com) for the virtualization part
- [CheerpX](https://cheerpx.io/), made by [Leaning Technologies](https://leaningtech.com/) for x86 virtualization and Linux emulation
- xterm.js, [https://xtermjs.org/](https://xtermjs.org/), for providing the Web-based terminal emulator
- [Tailscale](https://tailscale.com/) for the networking component
- [lwIP](https://savannah.nongnu.org/projects/lwip/) for the TCP/IP stack, compiled to the Web by [Cheerp](https://github.com/leaningtech/cheerp-meta)
- [Tailscale](https://tailscale.com/), for the networking component
- [lwIP](https://savannah.nongnu.org/projects/lwip/), for the TCP/IP stack, compiled for the Web via [Cheerp](https://github.com/leaningtech/cheerp-meta/)
# Versioning
WebVM depends on the CheerpX x86-to-WebAssembly virtualization technology, which is included in the project via [NPM](https://www.npmjs.com/package/@leaningtech/cheerpx).
The NPM package is updated on every release.
Every build is immutable, if a specific version works well for you today, it will keep working forever.
# License
Copyright (c) Leaning Technologies Limited. All rights reserved.
WebVM is released under the Apache License, Version 2.0.
You are welcome to use, modify, and redistribute the contents of this repository.
The public CheerpX deployment is provided **as-is** and is **free to use** for technological exploration, testing and use by individuals. Any other use by organizations, including non-profit, academia and the public sector, requires a license. Downloading a CheerpX build for the purpose of hosting it elsewhere is not permitted without a commercial license.
Read more about [CheerpX licensing](https://cheerpx.io/docs/licensing)
If you want to build a product on top of CheerpX/WebVM, please get in touch: sales@leaningtech.com

BIN
assets/alpine_bg.png Normal file

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

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"><defs><style>.cls-1{fill:#5865f2;}</style></defs><g id="图层_2" data-name="图层 2"><g id="Discord_Logos" data-name="Discord Logos"><g id="Discord_Logo_-_Large_-_White" data-name="Discord Logo - Large - White"><path class="cls-1" d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 988 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View file

@ -0,0 +1 @@
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 960 B

BIN
assets/result.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

BIN
assets/social_2024.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

11
assets/tailscale.svg Normal file
View file

@ -0,0 +1,11 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="2.89214" cy="11.7148" rx="2.89214" ry="2.88514" fill="white"></ellipse>
<ellipse cx="11.5685" cy="11.7148" rx="2.89214" ry="2.88514" fill="white"></ellipse>
<ellipse opacity="0.2" cx="2.89214" cy="20.3703" rx="2.89214" ry="2.88514" fill="white"></ellipse>
<ellipse opacity="0.2" cx="20.245" cy="20.3703" rx="2.89214" ry="2.88514" fill="white"></ellipse>
<ellipse cx="11.5685" cy="20.3703" rx="2.89214" ry="2.88514" fill="white"></ellipse>
<ellipse cx="20.245" cy="11.7148" rx="2.89214" ry="2.88514" fill="white"></ellipse>
<ellipse opacity="0.2" cx="2.89214" cy="3.0594" rx="2.89214" ry="2.88514" fill="white"></ellipse>
<ellipse opacity="0.2" cx="11.5685" cy="3.0594" rx="2.89214" ry="2.88514" fill="white"></ellipse>
<ellipse opacity="0.2" cx="20.245" cy="3.0594" rx="2.89214" ry="2.88514" fill="white"></ellipse>
</svg>

After

Width:  |  Height:  |  Size: 1,007 B

BIN
assets/webvm_hero.png Normal file

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: 77 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

@ -0,0 +1 @@
.dockerignore

19
dockerfiles/debian_large Normal file
View file

@ -0,0 +1,19 @@
FROM --platform=i386 i386/debian:buster
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get -y upgrade && \
apt-get install -y apt-utils beef bsdgames bsdmainutils ca-certificates clang \
cowsay cpio cron curl dmidecode dmsetup g++ gcc gdbm-l10n git \
hexedit ifupdown init logrotate lsb-base lshw lua50 luajit lynx make \
nano netbase nodejs openssl procps python3 python3-cryptography \
python3-jinja2 python3-numpy python3-pandas python3-pip python3-scipy \
python3-six python3-yaml readline-common rsyslog ruby sensible-utils \
ssh systemd systemd-sysv tasksel tasksel-data udev vim wget whiptail \
xxd iptables isc-dhcp-client isc-dhcp-common kmod less netcat-openbsd
# Make a user, then copy over the /example directory
RUN useradd -m user && echo "user:password" | chpasswd
COPY --chown=user:user ./examples /home/user/examples
RUN chmod -R +x /home/user/examples/lua
RUN echo 'root:password' | chpasswd
CMD [ "/bin/bash" ]

18
dockerfiles/debian_mini Normal file
View file

@ -0,0 +1,18 @@
FROM --platform=i386 i386/debian:buster
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get clean && apt-get update && apt-get -y upgrade
RUN apt-get -y install apt-utils gcc \
python3 vim unzip ruby nodejs \
fakeroot dbus base whiptail hexedit \
patch wamerican ucf manpages \
file luajit make lua50 dialog curl \
less cowsay netcat-openbsd
RUN useradd -m user && echo "user:password" | chpasswd
COPY --chown=user:user ./examples /home/user/examples
RUN chmod -R +x /home/user/examples/lua
# We set WORKDIR, as this gets extracted by Webvm to be used as the cwd. This is optional.
WORKDIR /home/user/
# We set env, as this gets extracted by Webvm. This is optional.
ENV HOME="/home/user" TERM="xterm" USER="user" SHELL="/bin/bash" EDITOR="vim" LANG="en_US.UTF-8" LC_ALL="C"
RUN echo 'root:password' | chpasswd
CMD [ "/bin/bash" ]

22
docs/Tailscale.md Normal file
View file

@ -0,0 +1,22 @@
# Enable networking
- In order to access the public internet, you will need an Exit Node. See [Tailscale Exit Nodes](https://tailscale.com/kb/1103/exit-nodes/) for detailed instructions.
- ***Note:*** This is not required to access machines in your own Tailscale Network.
- Depending on your network speed, you may need to wait a few moments for the Tailscale Wasm module to be downloaded.
**When all set:**
- Log in with your Tailscale credentials.
- Go back to the WebVM tab.
- The `Connect to Tailscale` button in the Networking side-panel should be replaced by your IP address.
# Log in to Tailscale with an Auth key
- Add `#authKey=<your-key>` at the end of the URL.
- Done, you don't need to manually log in anymore.
It is recommended to use an ephemeral key.
# Log in to a self-hosted Tailscale network (Headscale)
- Add `#controlUrl=<your-control-url>` at the end of the URL.
- You can combine this option with `authKey` with a `&`: `#controlUrl=<url>&authKey=<key>`.

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,3 +1,4 @@
#!/usr/bin/env luajit
fruits = {"banana","orange","apple","grapes"}
for k,v in ipairs(fruits) do

View file

@ -1,3 +1,4 @@
#!/usr/bin/env luajit
A = { ["John"] = true, ["Bob"] = true, ["Mary"] = true, ["Elena"] = true }
B = { ["Jim"] = true, ["Mary"] = true, ["John"] = true, ["Bob"] = true }

View file

@ -1,301 +0,0 @@
<!DOCTYPE html>
<!-- Serviceworker script that adds the COI and CORS headers to the response headers in cases where the server does not support it. -->
<script src="serviceWorker.js"></script>
<html lang="en" style="height:100%;">
<head>
<meta charset="utf-8">
<title>WebVM - Linux virtualization in WebAssembly</title>
<meta name="description" content="Server-less virtual machine, networking included, running browser-side in HTML5/WebAssembly. Code in any programming language inside this Linux terminal.">
<meta name="keywords" content="WebVM, Virtual Machine, CheerpX, x86 virtualization, WebAssembly, Tailscale, JIT">
<meta property="og:title" content="WebVM - Linux virtualization in WebAssembly" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="WebVM"/>
<meta property="og:url" content="/">
<meta property="og:image" content="https://webvm.io/assets/welcome_to_WebVM_.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@leaningtech" />
<meta name="twitter:title" content="WebVM - Linux virtualization in WebAssembly" />
<meta name="twitter:description" content="Server-less virtual machine, networking included, running browser-side in HTML5/WebAssembly. Code in any programming language inside this Linux terminal.">
<meta name="twitter:image" content="https://webvm.io/assets/welcome_to_WebVM_.png" />
<!-- Apple iOS web clip compatibility tags -->
<meta name="application-name" content="WebVM" />
<meta name="apple-mobile-web-app-title" content="WebVM" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<link rel="shortcut icon" href="./tower.ico">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" id="us-fonts-css" href="https://fonts.googleapis.com/css?family=Montserrat%3A300%2C400%2C500%2C600%2C700&amp;display=swap&amp;ver=6.0.2" media="all">
<link rel="stylesheet" href="./xterm/xterm.css" />
<link rel="stylesheet" href="./scrollbar.css" />
<script src="./xterm/xterm.js"></script>
<script src="./xterm/xterm-addon-fit.js"></script>
<script>
window.networkInterface = { ready: false };
</script>
<script type="module" src="network.js"></script>
</head>
<body style="margin:0;height:100%;background:black;color:white;overflow:hidden; display:flex; flex-direction: column; justify-content: space-between; height: 100%;">
<header style="flex-grow:0; flex-srink: 0;height:80px; width: 100%; margin: 2px 0 2px 0;">
<div style="display: flex; flex-direction: row; justify-content: space-between; width: 100%;">
<div style="display: flex; flex-direction: row;">
<a href="https://leaningtech.com/" target="_blank">
<img src="./assets/leaningtech.png" style="margin-left: 10px; height: 60px; margin-top: 10px;">
</a>
</div>
<div style="display: flex; flex-direction: row; justify-content: space-before;">
<li style=" margin-right: 50px; height: 100%; display: flex; align-items: center;">
<a href="https://discord.leaningtech.com" target="_blank" style="text-decoration: none">
<div style="color: white; font-family: montserrat; font-weight: 700; font-size: large;">Discord</div>
</a>
</li>
<li style=" margin-right: 50px; height: 100%; display: flex; align-items: center;">
<a href="https://github.com/leaningtech/webvm" target="_blank" style="text-decoration: none" >
<div style="color: white; font-family: montserrat; font-weight: 700; font-size: large;">Github</div>
</a>
</li>
<li style=" margin-right: 50px; height: 100%; display: flex; align-items: center;">
<a id="loginLink" style="text-decoration: none; cursor:not-allowed;">
<div id="networkStatus" style="color: grey; font-family: montserrat; font-weight: 700; font-size: large;">Tailscale Login</div>
</a>
</li>
</div>
</div>
</header>
<div style="flex-grow:0; flex-shrink: 0; height:1px; width: 100%; background-color: white;">
</div>
<main style="display: flex; flex-direction: row; justify-content: space-between; margin:0; height:100%;">
<div style="flex-grow:1; height:100%;display:inline-block;margin:0;" class="scrollbar" id="console">
</div>
</main>
<script>
//Utility namespace to group all functionality related to printing (both error and non error) messages
const color= "\x1b[1;35m";
const bold= "\x1b[1;37m";
const underline= "\x1b[94;4m";
const normal= "\x1b[0m";
var printOnTerm = {
getAsciiTitle: function ()
{
var title = [
color + " __ __ _ __ ____ __ " + normal,
color + " \\ \\ / /__| |_\\ \\ / / \\/ | " + normal,
color + " \\ \\/\\/ / -_) '_ \\ V /| |\\/| | " + normal,
color + " \\_/\\_/\\___|_.__/\\_/ |_| |_| " + normal,
];
return title;
},
getAsciiText: function ()
{
var text = [
"+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+",
"| |",
"| WebVM is a server-less virtual Linux environment running fully client-side |",
"| in HTML5/WebAssembly. |",
"| |",
"| WebVM is powered by the CheerpX virtualization engine, which enables safe, |",
"| sandboxed client-side execution of x86 binaries on any browser. |",
"| |",
"| CheerpX includes an x86-to-WebAssembly JIT compiler, a virtual block-based |",
"| file system, and a Linux syscall emulator. |",
"| |",
"| [NEW!] WebVM now supports full TCP and UDP networking via Tailscale! |",
"| Click on 'Tailscale Login' to enable it. Read the announcement: |",
"| |",
"| " + underline + "https://leaningtech.com/webvm-virtual-machine-with-networking-via-tailscale" + normal +" |",
"| |",
"+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+",
"",
" Welcome to WebVM (build CX_VERSION). If unsure, try these examples:",
"",
" python3 examples/python3/fibonacci.py ",
" gcc -o helloworld examples/c/helloworld.c && ./helloworld",
" objdump -d ./helloworld | less -M",
" vim examples/c/helloworld.c",
" curl --max-time 15 parrot.live # requires networking",
"",
];
return text;
},
getSharedArrayBufferMissingMessage: function ()
{
const text = [
"",
"",
color + "CheerpX could not start" + normal,
"",
"CheerpX depends on JavaScript's SharedArrayBuffer, that your browser",
" does not support.",
"",
"SharedArrayBuffer is currently enabled by default on recent",
" versions of Chrome, Edge, Firefox and Safari.",
"",
"",
"Give it a try from another browser!",
]
return text;
},
getErrorMessage: function (error_message)
{
const text = [
"",
"",
color + "CheerpX could not start" + normal,
"",
"CheerpX internal error message is:",
error_message,
"",
"",
"CheerpX is expected to work with recent desktop versions of Chrome, Edge, Firefox and Safari",
"",
"",
"Give it a try from a desktop version / another browser!",
]
return text;
},
printMessage: function (text) {
for (var i=0; i<text.length; i++)
{
term.write(text[i]);
term.write('\n');
}
},
printError: function (message)
{
this.printMessage(message);
term.write("\n\n");
function writeCustom(something)
{
term.write(something);
}
},
};
var consoleDiv = document.getElementById("console");
//xterm.js related logic
var term = new Terminal({cursorBlink:true,convertEol:true, fontFamily:"monospace", fontWeight: 400, fontWeightBold: 700});
var fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
term.open(consoleDiv);
term.scrollToTop();
fitAddon.fit();
window.addEventListener("resize", function(ev){fitAddon.fit();}, false);
term.focus();
var cxReadFunc = null;
function writeData(buf)
{
term.write(new Uint8Array(buf));
}
function readData(str)
{
if(cxReadFunc == null)
return;
for(var i=0;i<str.length;i++)
cxReadFunc(str.charCodeAt(i));
}
term.onData(readData);
//Actual CheerpX and bash specific logic
function runBash()
{
const structure = {
name: "bash",
cmd: "/bin/bash",
args: ["--login"],
env: ["HOME=/home/user", "TERM=xterm", "USER=user", "SHELL=/bin/bash", "EDITOR=vim", "LANG=en_US.UTF-8", "LC_ALL=C"],
expectedPrompt: ">",
versionOpt: "--version",
comment_line: "#",
description_line: "The original Bourne Again SHell",
}
if (typeof SharedArrayBuffer === "undefined")
{
printOnTerm.printError(printOnTerm.getSharedArrayBufferMissingMessage());
return;
}
async function runTest(cx)
{
term.scrollToBottom();
async function cxLogAndRun(cheerpx, cmd, args, env)
{
await cheerpx.run(cmd, args, env);
printOnTerm.printMessage(" ");
}
cxReadFunc = cx.setCustomConsole(writeData, term.cols, term.rows);
function preventDefaults (e) {
e.preventDefault()
e.stopPropagation()
}
consoleDiv.addEventListener("dragover", preventDefaults, false);
consoleDiv.addEventListener("dragenter", preventDefaults, false);
consoleDiv.addEventListener("dragleave", preventDefaults, false);
consoleDiv.addEventListener("drop", preventDefaults, false);
var opts = {env:structure.env, cwd:"/home/user"};
while (true)
{
await cxLogAndRun(cx, structure.cmd, structure.args, opts);
}
}
function failCallback(err)
{
printOnTerm.printError(printOnTerm.getErrorMessage(err));
}
CheerpXApp.create({devices:[{type:"block",url:"https://disks.leaningtech.com/webvm_20221004.ext2",name:"block1"}],mounts:[{type:"ext2",dev:"block1",path:"/"},{type:"cheerpOS",dev:"/app",path:"/app"},{type:"cheerpOS",dev:"/str",path:"/data"},{type:"devs",dev:"",path:"/dev"}], networkInterface}).then(runTest, failCallback);
}
function initialMessage()
{
printOnTerm.printMessage(printOnTerm.getAsciiTitle());
printOnTerm.printMessage([""]);
printOnTerm.printMessage(printOnTerm.getAsciiText());
term.registerLinkMatcher(/https:\/\/leaningtech.com\/webvm-virtual-machine-with-networking-via-tailscale/, function(mouseEvent, matchedString) {
window.open(matchedString, "_blank")
});
console.log("Welcome. We appreciate curiosity, but be warned that keeping the DevTools open causes significant performance degradation and crashes.");
}
initialMessage();
var script = document.createElement('script');
script.type = 'text/javascript';
var cxFile = "https://cheerpxdemos.leaningtech.com/publicdeploy/CX_VERSION/cx.js";
script.src = cxFile;
script.addEventListener("load", runBash, false);
document.head.appendChild(script);
</script>
<!-- Google tag (gtag.js) -->
<script defer src="https://www.googletagmanager.com/gtag/js?id=G-818T3Y0PEY"></script>
<script defer>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-818T3Y0PEY');
</script>
</body>
</html>

View file

@ -1,85 +0,0 @@
import { State } from "./tun/tailscale_tun.js";
import { autoConf } from "./tun/tailscale_tun_auto.js";
let params = new URLSearchParams("?"+window.location.hash.substr(1));
let authKey = params.get("authKey") || undefined;
let controlUrl = params.get("controlUrl") || undefined;
console.log(authKey, controlUrl);
let loginElemUrl = controlUrl ? null : "https://login.tailscale.com/admin/machines";
let resolveLogin = null;
let loginPromise = new Promise((f,r) => {
resolveLogin = f;
});
const loginElem = document.getElementById("loginLink");
const statusElem = document.getElementById("networkStatus");
const loginUrlCb = (url) => {
loginElem.href = url;
loginElem.target = "_blank";
statusElem.innerHTML = "Tailscale Login";
resolveLogin(url);
};
const stateUpdateCb = (state) => {
switch(state)
{
case State.NeedsLogin:
{
break;
}
case State.Running:
{
if (loginElemUrl) {
loginElem.href = loginElemUrl;
}
break;
}
case State.Starting:
{
break;
}
case State.Stopped:
{
break;
}
case State.NoState:
{
break;
}
}
};
const netmapUpdateCb = (map) => {
const ip = map.self.addresses[0];
statusElem.innerHTML = "IP: "+ip;
};
const { tcpSocket, udpSocket, up } = await autoConf({
loginUrlCb,
stateUpdateCb,
netmapUpdateCb,
authKey,
controlUrl,
});
window.networkInterface.tcpSocket = tcpSocket;
window.networkInterface.udpSocket = udpSocket;
window.networkInterface.ready = true;
loginElem.style.cursor = "pointer";
statusElem.style.color = "white";
if (authKey) {
if (loginElemUrl) {
loginElem.href = loginElemUrl;
loginElem.target = "_blank";
}
up();
} else {
loginElem.onclick = () => {
loginElem.onclick = null;
statusElem.innerHTML = "Downloading network code...";
const w = window.open("login.html", "_blank");
async function waitLogin() {
await up();
statusElem.innerHTML = "Starting login...";
const url = await loginPromise;
w.location.href = url;
}
waitLogin();
};
}

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;
}}
},
}

View file

@ -55,9 +55,14 @@ async function doRegister() {
// f.e on first access.
registration.addEventListener("updatefound", () => {
console.log("Reloading the page to transfer control to the Service Worker.");
window.location.reload();
try {
window.location.reload();
} catch (err) {
console.log("Service Worker failed reloading the page. ERROR:" + err);
};
});
// If the registration is active, but it's not controlling the page, reload the page to have it take control
// When the registration is active, but it's not controlling the page, we reload the page to have it take control.
// This f.e occurs when you hard-reload (shift + refresh). https://www.w3.org/TR/service-workers/#navigator-service-worker-controller
if (registration.active && !navigator.serviceWorker.controller) {
console.log("Reloading the page to transfer control to the Service Worker.");
try {

38
src/app.html Normal file
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: [],
}

View file

@ -1,275 +0,0 @@
<!DOCTYPE html>
<!-- Serviceworker script that adds the COI and CORS headers to the response headers in cases where the server does not support it. -->
<script src="serviceWorker.js"></script>
<html lang="en" style="height:100%;">
<head>
<meta charset="utf-8">
<title>WebVM - Linux virtualization in WebAssembly</title>
<meta name="description" content="Server-less virtual machine, networking included, running browser-side in HTML5/WebAssembly. Code in any programming language inside this Linux terminal.">
<meta name="keywords" content="WebVM, Virtual Machine, CheerpX, x86 virtualization, WebAssembly, Tailscale, JIT">
<meta property="og:title" content="WebVM - Linux virtualization in WebAssembly" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="WebVM"/>
<meta property="og:url" content="/">
<meta property="og:image" content="https://webvm.io/assets/welcome_to_WebVM_.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@leaningtech" />
<meta name="twitter:title" content="WebVM - Linux virtualization in WebAssembly" />
<meta name="twitter:description" content="Server-less virtual machine, networking included, running browser-side in HTML5/WebAssembly. Code in any programming language inside this Linux terminal.">
<meta name="twitter:image" content="https://webvm.io/assets/welcome_to_WebVM_.png" />
<!-- Apple iOS web clip compatibility tags -->
<meta name="application-name" content="WebVM" />
<meta name="apple-mobile-web-app-title" content="WebVM" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<link rel="shortcut icon" href="./tower.ico">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" id="us-fonts-css" href="https://fonts.googleapis.com/css?family=Montserrat%3A300%2C400%2C500%2C600%2C700&amp;display=swap&amp;ver=6.0.2" media="all">
<link rel="stylesheet" href="./xterm/xterm.css" />
<link rel="stylesheet" href="./scrollbar.css" />
<script src="./xterm/xterm.js"></script>
<script src="./xterm/xterm-addon-fit.js"></script>
<script>
window.networkInterface = { ready: false };
</script>
<script type="module" src="network.js"></script>
</head>
<body style="margin:0;height:100%;background:black;color:white;overflow:hidden; display:flex; flex-direction: column; justify-content: space-between; height: 100%;">
<header style="flex-grow:0; flex-srink: 0;height:80px; width: 100%; margin: 2px 0 2px 0;">
<div style="display: flex; flex-direction: row; justify-content: space-between; width: 100%;">
<div style="display: flex; flex-direction: row;">
<a href="https://leaningtech.com/" target="_blank">
<img src="./assets/leaningtech.png" style="margin-left: 10px; height: 60px; margin-top: 10px;">
</a>
</div>
<div style="display: flex; flex-direction: row; justify-content: space-before;">
<li style=" margin-right: 50px; height: 100%; display: flex; align-items: center;">
<a href="https://discord.leaningtech.com" target="_blank" style="text-decoration: none">
<div style="color: white; font-family: montserrat; font-weight: 700; font-size: large;">Discord</div>
</a>
</li>
<li style=" margin-right: 50px; height: 100%; display: flex; align-items: center;">
<a href="https://github.com/leaningtech/webvm" target="_blank" style="text-decoration: none" >
<div style="color: white; font-family: montserrat; font-weight: 700; font-size: large;">Github</div>
</a>
</li>
<li style=" margin-right: 50px; height: 100%; display: flex; align-items: center;">
<a id="loginLink" style="text-decoration: none; cursor:not-allowed;">
<div id="networkStatus" style="color: grey; font-family: montserrat; font-weight: 700; font-size: large;">Tailscale Login</div>
</a>
</li>
</div>
</div>
</header>
<div style="flex-grow:0; flex-shrink: 0; height:1px; width: 100%; background-color: white;">
</div>
<main style="display: flex; flex-direction: row; justify-content: space-between; margin:0; height:100%;">
<div style="flex-grow:1; height:100%;display:inline-block;margin:0;" class="scrollbar" id="console">
</div>
</main>
<script>
//Utility namespace to group all functionality related to printing (both error and non error) messages
const color= "\x1b[1;35m";
const bold= "\x1b[1;37m";
const underline= "\x1b[94;4m";
const normal= "\x1b[0m";
var printOnTerm = {
getAsciiTitle: function ()
{
var title = [
color + " __ __ _ __ ____ __ " + normal,
color + " \\ \\ / /__| |_\\ \\ / / \\/ | " + normal,
color + " \\ \\/\\/ / -_) '_ \\ V /| |\\/| | " + normal,
color + " \\_/\\_/\\___|_.__/\\_/ |_| |_| " + normal,
];
return title;
},
getAsciiText: function ()
{
var text = [
"+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+",
"| |",
"| WebVM is a server-less virtual Linux environment running fully client-side |",
"| in HTML5/WebAssembly. |",
"| |",
"| WebVM is powered by the CheerpX virtualization engine, which enables safe, |",
"| sandboxed client-side execution of x86 binaries on any browser. |",
"| |",
"| CheerpX includes an x86-to-WebAssembly JIT compiler, a virtual block-based |",
"| file system, and a Linux syscall emulator. |",
"| |",
"| [NEW!] WebVM now supports full TCP and UDP networking via Tailscale! |",
"| Click on 'Tailscale Login' to enable it. Read the announcement: |",
"| |",
"| " + underline + "https://leaningtech.com/webvm-virtual-machine-with-networking-via-tailscale" + normal +" |",
"| |",
"+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+",
"",
" Welcome to WebVM (build CX_VERSION). If unsure, try these examples:",
"",
" python3 examples/python3/fibonacci.py ",
" gcc -o helloworld examples/c/helloworld.c && ./helloworld",
" objdump -d ./helloworld | less -M",
" vim examples/c/helloworld.c",
" curl --max-time 15 parrot.live # requires networking",
"",
];
return text;
},
getSharedArrayBufferMissingMessage: function ()
{
const text = [
"",
"",
color + "CheerpX could not start" + normal,
"",
"CheerpX depends on JavaScript's SharedArrayBuffer, that your browser",
" does not support.",
"",
"SharedArrayBuffer is currently enabled by default on recent",
" versions of Chrome, Edge, Firefox and Safari.",
"",
"",
"Give it a try from another browser!",
]
return text;
},
getErrorMessage: function (error_message)
{
const text = [
"",
"",
color + "CheerpX could not start" + normal,
"",
"CheerpX internal error message is:",
error_message,
"",
"",
"CheerpX is expected to work with recent desktop versions of Chrome, Edge, Firefox and Safari",
"",
"",
"Give it a try from a desktop version / another browser!",
]
return text;
},
printMessage: function (text) {
for (var i=0; i<text.length; i++)
{
term.write(text[i]);
term.write('\n');
}
},
printError: function (message)
{
this.printMessage(message);
term.write("\n\n");
function writeCustom(something)
{
term.write(something);
}
},
};
var consoleDiv = document.getElementById("console");
//xterm.js related logic
var term = new Terminal({cursorBlink:true,convertEol:true, fontFamily:"monospace", fontWeight: 400, fontWeightBold: 700});
var fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
term.open(consoleDiv);
term.scrollToTop();
fitAddon.fit();
window.addEventListener("resize", function(ev){fitAddon.fit();}, false);
term.focus();
var cxReadFunc = null;
function writeData(buf)
{
term.write(new Uint8Array(buf));
}
function readData(str)
{
if(cxReadFunc == null)
return;
for(var i=0;i<str.length;i++)
cxReadFunc(str.charCodeAt(i));
}
term.onData(readData);
//Actual CheerpX and init specific logic
function runInit()
{
if (typeof SharedArrayBuffer === "undefined")
{
printOnTerm.printError(printOnTerm.getSharedArrayBufferMissingMessage());
return;
}
async function runTest(cx)
{
term.scrollToBottom();
cxReadFunc = cx.setCustomConsole(writeData, term.cols, term.rows);
function preventDefaults (e) {
e.preventDefault()
e.stopPropagation()
}
consoleDiv.addEventListener("dragover", preventDefaults, false);
consoleDiv.addEventListener("dragenter", preventDefaults, false);
consoleDiv.addEventListener("dragleave", preventDefaults, false);
consoleDiv.addEventListener("drop", preventDefaults, false);
var opts = {uid:0};
cx.run("/init", [], opts);
}
function failCallback(err)
{
printOnTerm.printError(printOnTerm.getErrorMessage(err));
}
CheerpXApp.create({devices:[{type:"block",url:"https://disks.leaningtech.com/tc_nographic_20221117.ext2",name:"block1"}],mounts:[{type:"ext2",dev:"block1",path:"/"},{type:"cheerpOS",dev:"/app",path:"/app"}], networkInterface}).then(runTest, failCallback);
}
function initialMessage()
{
console.log("Welcome. We appreciate curiosity, but be warned that keeping the DevTools open causes significant performance degradation and crashes.");
}
initialMessage();
var script = document.createElement('script');
script.type = 'text/javascript';
var cxFile = "https://cheerpxdemos.leaningtech.com/publicdeploy/CX_VERSION/cx.js";
script.src = cxFile;
script.addEventListener("load", runInit, false);
document.head.appendChild(script);
</script>
<!-- Google tag (gtag.js) -->
<script defer src="https://www.googletagmanager.com/gtag/js?id=G-818T3Y0PEY"></script>
<script defer>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-818T3Y0PEY');
</script>
</body>
</html>

File diff suppressed because it is too large Load diff

Binary file not shown.

Binary file not shown.

View file

@ -1,107 +0,0 @@
import "./wasm_exec.js";
import ipStackAwait from "./ipstack.js";
export const State = {
NoState: 0,
InUseOtherUser: 1,
NeedsLogin: 2,
NeedsMachineAuth: 3,
Stopped: 4,
Starting: 5,
Running: 6,
};
export async function init() {
const {IpStack} = await ipStackAwait();
IpStack.init();
const listeners = {
onstateupdate: () => {},
onnetmap: () => {},
onloginurl: () => {},
}
let ipn = null;
let localIp = null;
let dnsIp = null;
const lazyRunIpn = async () => {
const wasmUrl = new URL("tailscale.wasm", import.meta.url);
const go = new window.Go();
let {instance} = await fetch(wasmUrl).then(x => x.arrayBuffer()).then(x => WebAssembly.instantiate(x,go.importObject));
go.run(instance);
const sessionStateStorage = {
setState(id, value) {
window.sessionStorage[`ipn-state-${id}`] = value
},
getState(id) {
return window.sessionStorage[`ipn-state-${id}`] || ""
},
}
ipn = newIPN({
// Persist IPN state in sessionStorage in development, so that we don't need
// to re-authorize every time we reload the page.
//stateStorage: sessionStateStorage,
});
const setupIpStack = () => {
ipn.tun.onmessage = function(ev) {
IpStack.input(ev.data)
};
IpStack.output(function(p){
ipn.tun.postMessage(p, [p.buffer]);
});
};
setupIpStack();
ipn.run({
notifyState: (s) => listeners.onstateupdate(s),
notifyNetMap: (s) => {
const netMap = JSON.parse(s);
listeners.onnetmap(netMap);
const newLocalIp = netMap.self.addresses[0];
if (localIp != newLocalIp)
{
localIp = newLocalIp;
try{
IpStack.up({localIp, dnsIp, ipMap: {
["127.0.0.53"]: dnsIp,
[dnsIp]: "127.0.0.53",
}});
}catch(e){
console.log(e);
debugger;
}
}
},
notifyBrowseToURL: (l) => listeners.onloginurl(l),
});
};
return {
tcpSocket: IpStack.TCPSocket.create,
udpSocket: IpStack.UDPSocket.create,
parseIP: IpStack.parseIP,
resolve: IpStack.resolve,
up: async (conf) => {
if (ipn == null) {
await lazyRunIpn();
}
ipn.up(conf);
localIp = null;
dnsIp = conf.dnsIp || "127.0.0.53";
},
down: () => {
ipn.down();
IpStack.down();
},
login: () => ipn.login(),
logout: () => ipn.logout(),
listeners
};
}

View file

@ -1,77 +0,0 @@
import {State, init} from "./tailscale_tun.js";
export async function autoConf({loginUrlCb, stateUpdateCb, netmapUpdateCb, controlUrl, authKey}) {
const { tcpSocket, udpSocket, parseIP, resolve, up, down, login, logout, listeners } = await init();
const settings = {
controlUrl: controlUrl,
authKey: authKey,
exitNodeIp: undefined,
dnsIp: undefined,
wantsRunning: true,
};
listeners.onstateupdate = (state) => {
stateUpdateCb(state);
switch(state)
{
case State.NeedsLogin:
{
login();
break;
}
case State.Running:
{
break;
}
case State.Starting:
{
break;
}
case State.Stopped:
{
break;
}
case State.NoState:
{
up(settings);
break;
}
default:
{
console.log(state);
break;
}
}
};
listeners.onloginurl = (login) => {
console.log("login url:",login);
loginUrlCb(login);
};
listeners.onnetmap = (map) => {
netmapUpdateCb(map);
if (!settings.exitNodeIp) {
for (let p of map.peers) {
if (p.online && p.exitNode) {
settings.exitNodeIp = p.addresses[0];
settings.dnsIp = "8.8.8.8";
up(settings);
}
}
}
};
return {
tcpSocket,
udpSocket,
parseIP,
resolve,
up: async () => {
await up(settings);
},
}
}

View file

@ -1,554 +0,0 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
"use strict";
(() => {
const enosys = () => {
const err = new Error("not implemented");
err.code = "ENOSYS";
return err;
};
if (!globalThis.fs) {
let outputBuf = "";
globalThis.fs = {
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
writeSync(fd, buf) {
outputBuf += decoder.decode(buf);
const nl = outputBuf.lastIndexOf("\n");
if (nl != -1) {
console.log(outputBuf.substr(0, nl));
outputBuf = outputBuf.substr(nl + 1);
}
return buf.length;
},
write(fd, buf, offset, length, position, callback) {
if (offset !== 0 || length !== buf.length || position !== null) {
callback(enosys());
return;
}
const n = this.writeSync(fd, buf);
callback(null, n);
},
chmod(path, mode, callback) { callback(enosys()); },
chown(path, uid, gid, callback) { callback(enosys()); },
close(fd, callback) { callback(enosys()); },
fchmod(fd, mode, callback) { callback(enosys()); },
fchown(fd, uid, gid, callback) { callback(enosys()); },
fstat(fd, callback) { callback(enosys()); },
fsync(fd, callback) { callback(null); },
ftruncate(fd, length, callback) { callback(enosys()); },
lchown(path, uid, gid, callback) { callback(enosys()); },
link(path, link, callback) { callback(enosys()); },
lstat(path, callback) { callback(enosys()); },
mkdir(path, perm, callback) { callback(enosys()); },
open(path, flags, mode, callback) { callback(enosys()); },
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
readdir(path, callback) { callback(enosys()); },
readlink(path, callback) { callback(enosys()); },
rename(from, to, callback) { callback(enosys()); },
rmdir(path, callback) { callback(enosys()); },
stat(path, callback) { callback(enosys()); },
symlink(path, link, callback) { callback(enosys()); },
truncate(path, length, callback) { callback(enosys()); },
unlink(path, callback) { callback(enosys()); },
utimes(path, atime, mtime, callback) { callback(enosys()); },
};
}
if (!globalThis.process) {
globalThis.process = {
getuid() { return -1; },
getgid() { return -1; },
geteuid() { return -1; },
getegid() { return -1; },
getgroups() { throw enosys(); },
pid: -1,
ppid: -1,
umask() { throw enosys(); },
cwd() { throw enosys(); },
chdir() { throw enosys(); },
}
}
if (!globalThis.crypto) {
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
}
if (!globalThis.performance) {
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
}
if (!globalThis.TextEncoder) {
throw new Error("globalThis.TextEncoder is not available, polyfill required");
}
if (!globalThis.TextDecoder) {
throw new Error("globalThis.TextDecoder is not available, polyfill required");
}
const encoder = new TextEncoder("utf-8");
const decoder = new TextDecoder("utf-8");
globalThis.Go = class {
constructor() {
this.argv = ["js"];
this.env = {};
this.exit = (code) => {
if (code !== 0) {
console.warn("exit code:", code);
}
};
this._exitPromise = new Promise((resolve) => {
this._resolveExitPromise = resolve;
});
this._pendingEvent = null;
this._scheduledTimeouts = new Map();
this._nextCallbackTimeoutID = 1;
const setInt64 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
}
const getInt64 = (addr) => {
const low = this.mem.getUint32(addr + 0, true);
const high = this.mem.getInt32(addr + 4, true);
return low + high * 4294967296;
}
const loadValue = (addr) => {
const f = this.mem.getFloat64(addr, true);
if (f === 0) {
return undefined;
}
if (!isNaN(f)) {
return f;
}
const id = this.mem.getUint32(addr, true);
return this._values[id];
}
const storeValue = (addr, v) => {
const nanHead = 0x7FF80000;
if (typeof v === "number" && v !== 0) {
if (isNaN(v)) {
this.mem.setUint32(addr + 4, nanHead, true);
this.mem.setUint32(addr, 0, true);
return;
}
this.mem.setFloat64(addr, v, true);
return;
}
if (v === undefined) {
this.mem.setFloat64(addr, 0, true);
return;
}
let id = this._ids.get(v);
if (id === undefined) {
id = this._idPool.pop();
if (id === undefined) {
id = this._values.length;
}
this._values[id] = v;
this._goRefCounts[id] = 0;
this._ids.set(v, id);
}
this._goRefCounts[id]++;
let typeFlag = 0;
switch (typeof v) {
case "object":
if (v !== null) {
typeFlag = 1;
}
break;
case "string":
typeFlag = 2;
break;
case "symbol":
typeFlag = 3;
break;
case "function":
typeFlag = 4;
break;
}
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
this.mem.setUint32(addr, id, true);
}
const loadSlice = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
}
const loadSliceOfValues = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
const a = new Array(len);
for (let i = 0; i < len; i++) {
a[i] = loadValue(array + i * 8);
}
return a;
}
const loadString = (addr) => {
const saddr = getInt64(addr + 0);
const len = getInt64(addr + 8);
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
}
const timeOrigin = Date.now() - performance.now();
this.importObject = {
go: {
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
// This changes the SP, thus we have to update the SP used by the imported function.
// func wasmExit(code int32)
"runtime.wasmExit": (sp) => {
sp >>>= 0;
const code = this.mem.getInt32(sp + 8, true);
this.exited = true;
delete this._inst;
delete this._values;
delete this._goRefCounts;
delete this._ids;
delete this._idPool;
this.exit(code);
},
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
"runtime.wasmWrite": (sp) => {
sp >>>= 0;
const fd = getInt64(sp + 8);
const p = getInt64(sp + 16);
const n = this.mem.getInt32(sp + 24, true);
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
},
// func resetMemoryDataView()
"runtime.resetMemoryDataView": (sp) => {
sp >>>= 0;
this.mem = new DataView(this._inst.exports.mem.buffer);
},
// func nanotime1() int64
"runtime.nanotime1": (sp) => {
sp >>>= 0;
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
},
// func walltime() (sec int64, nsec int32)
"runtime.walltime": (sp) => {
sp >>>= 0;
const msec = (new Date).getTime();
setInt64(sp + 8, msec / 1000);
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
},
// func scheduleTimeoutEvent(delay int64) int32
"runtime.scheduleTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this._nextCallbackTimeoutID;
this._nextCallbackTimeoutID++;
this._scheduledTimeouts.set(id, setTimeout(
() => {
this._resume();
while (this._scheduledTimeouts.has(id)) {
// for some reason Go failed to register the timeout event, log and try again
// (temporary workaround for https://github.com/golang/go/issues/28975)
console.warn("scheduleTimeoutEvent: missed timeout event");
this._resume();
}
},
getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early
));
this.mem.setInt32(sp + 16, id, true);
},
// func clearTimeoutEvent(id int32)
"runtime.clearTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this.mem.getInt32(sp + 8, true);
clearTimeout(this._scheduledTimeouts.get(id));
this._scheduledTimeouts.delete(id);
},
// func getRandomData(r []byte)
"runtime.getRandomData": (sp) => {
sp >>>= 0;
crypto.getRandomValues(loadSlice(sp + 8));
},
// func finalizeRef(v ref)
"syscall/js.finalizeRef": (sp) => {
sp >>>= 0;
const id = this.mem.getUint32(sp + 8, true);
this._goRefCounts[id]--;
if (this._goRefCounts[id] === 0) {
const v = this._values[id];
this._values[id] = null;
this._ids.delete(v);
this._idPool.push(id);
}
},
// func stringVal(value string) ref
"syscall/js.stringVal": (sp) => {
sp >>>= 0;
storeValue(sp + 24, loadString(sp + 8));
},
// func valueGet(v ref, p string) ref
"syscall/js.valueGet": (sp) => {
sp >>>= 0;
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 32, result);
},
// func valueSet(v ref, p string, x ref)
"syscall/js.valueSet": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
},
// func valueDelete(v ref, p string)
"syscall/js.valueDelete": (sp) => {
sp >>>= 0;
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
},
// func valueIndex(v ref, i int) ref
"syscall/js.valueIndex": (sp) => {
sp >>>= 0;
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
},
// valueSetIndex(v ref, i int, x ref)
"syscall/js.valueSetIndex": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
},
// func valueCall(v ref, m string, args []ref) (ref, bool)
"syscall/js.valueCall": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const m = Reflect.get(v, loadString(sp + 16));
const args = loadSliceOfValues(sp + 32);
const result = Reflect.apply(m, v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, result);
this.mem.setUint8(sp + 64, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, err);
this.mem.setUint8(sp + 64, 0);
}
},
// func valueInvoke(v ref, args []ref) (ref, bool)
"syscall/js.valueInvoke": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.apply(v, undefined, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueNew(v ref, args []ref) (ref, bool)
"syscall/js.valueNew": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.construct(v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueLength(v ref) int
"syscall/js.valueLength": (sp) => {
sp >>>= 0;
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
},
// valuePrepareString(v ref) (ref, int)
"syscall/js.valuePrepareString": (sp) => {
sp >>>= 0;
const str = encoder.encode(String(loadValue(sp + 8)));
storeValue(sp + 16, str);
setInt64(sp + 24, str.length);
},
// valueLoadString(v ref, b []byte)
"syscall/js.valueLoadString": (sp) => {
sp >>>= 0;
const str = loadValue(sp + 8);
loadSlice(sp + 16).set(str);
},
// func valueInstanceOf(v ref, t ref) bool
"syscall/js.valueInstanceOf": (sp) => {
sp >>>= 0;
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
},
// func copyBytesToGo(dst []byte, src ref) (int, bool)
"syscall/js.copyBytesToGo": (sp) => {
sp >>>= 0;
const dst = loadSlice(sp + 8);
const src = loadValue(sp + 32);
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
// func copyBytesToJS(dst ref, src []byte) (int, bool)
"syscall/js.copyBytesToJS": (sp) => {
sp >>>= 0;
const dst = loadValue(sp + 8);
const src = loadSlice(sp + 16);
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
"debug": (value) => {
console.log(value);
},
}
};
}
async run(instance) {
if (!(instance instanceof WebAssembly.Instance)) {
throw new Error("Go.run: WebAssembly.Instance expected");
}
this._inst = instance;
this.mem = new DataView(this._inst.exports.mem.buffer);
this._values = [ // JS values that Go currently has references to, indexed by reference id
NaN,
0,
null,
true,
false,
globalThis,
this,
];
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
this._ids = new Map([ // mapping from JS values to reference ids
[0, 1],
[null, 2],
[true, 3],
[false, 4],
[globalThis, 5],
[this, 6],
]);
this._idPool = []; // unused ids that have been garbage collected
this.exited = false; // whether the Go program has exited
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
let offset = 4096;
const strPtr = (str) => {
const ptr = offset;
const bytes = encoder.encode(str + "\0");
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
offset += bytes.length;
if (offset % 8 !== 0) {
offset += 8 - (offset % 8);
}
return ptr;
};
const argc = this.argv.length;
const argvPtrs = [];
this.argv.forEach((arg) => {
argvPtrs.push(strPtr(arg));
});
argvPtrs.push(0);
const keys = Object.keys(this.env).sort();
keys.forEach((key) => {
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
});
argvPtrs.push(0);
const argv = offset;
argvPtrs.forEach((ptr) => {
this.mem.setUint32(offset, ptr, true);
this.mem.setUint32(offset + 4, 0, true);
offset += 8;
});
// The linker guarantees global data starts from at least wasmMinDataAddr.
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
const wasmMinDataAddr = 4096 + 8192;
if (offset >= wasmMinDataAddr) {
throw new Error("total length of command line and environment variables exceeds limit");
}
this._inst.exports.run(argc, argv);
if (this.exited) {
this._resolveExitPromise();
}
await this._exitPromise;
}
_resume() {
if (this.exited) {
throw new Error("Go program has already exited");
}
this._inst.exports.resume();
if (this.exited) {
this._resolveExitPromise();
}
}
_makeFuncWrapper(id) {
const go = this;
return function () {
const event = { id: id, this: this, args: arguments };
go._pendingEvent = event;
go._resume();
return event.result;
};
}
}
})();

28
vite.config.js Normal file
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: '' }
]
})
]
});

View file

@ -1 +1,2 @@
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(self,(function(){return(()=>{"use strict";var e={775:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0;var r=function(){function e(){}return e.prototype.activate=function(e){this._terminal=e},e.prototype.dispose=function(){},e.prototype.fit=function(){var e=this.proposeDimensions();if(e&&this._terminal){var t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}},e.prototype.proposeDimensions=function(){if(this._terminal&&this._terminal.element&&this._terminal.element.parentElement){var e=this._terminal._core;if(0!==e._renderService.dimensions.actualCellWidth&&0!==e._renderService.dimensions.actualCellHeight){var t=window.getComputedStyle(this._terminal.element.parentElement),r=parseInt(t.getPropertyValue("height")),i=Math.max(0,parseInt(t.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),o=r-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=i-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-e.viewport.scrollBarWidth;return{cols:Math.max(2,Math.floor(a/e._renderService.dimensions.actualCellWidth)),rows:Math.max(1,Math.floor(o/e._renderService.dimensions.actualCellHeight))}}}},e}();t.FitAddon=r}},t={};return function r(i){if(t[i])return t[i].exports;var n=t[i]={exports:{}};return e[i](n,n.exports,r),n.exports}(775)})()}));
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(self,(()=>(()=>{"use strict";var e={};return(()=>{var t=e;Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0,t.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,i=window.getComputedStyle(this._terminal.element.parentElement),o=parseInt(i.getPropertyValue("height")),s=Math.max(0,parseInt(i.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),l=o-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=s-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}}})(),e})()));
//# sourceMappingURL=addon-fit.js.map

View file

@ -0,0 +1,2 @@
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.WebLinksAddon=t():e.WebLinksAddon=t()}(self,(()=>(()=>{"use strict";var e={6:(e,t)=>{function n(e){try{const t=new URL(e),n=t.password&&t.username?`${t.protocol}//${t.username}:${t.password}@${t.host}`:t.username?`${t.protocol}//${t.username}@${t.host}`:`${t.protocol}//${t.host}`;return e.toLocaleLowerCase().startsWith(n.toLocaleLowerCase())}catch(e){return!1}}Object.defineProperty(t,"__esModule",{value:!0}),t.LinkComputer=t.WebLinkProvider=void 0,t.WebLinkProvider=class{constructor(e,t,n,o={}){this._terminal=e,this._regex=t,this._handler=n,this._options=o}provideLinks(e,t){const n=o.computeLink(e,this._regex,this._terminal,this._handler);t(this._addCallbacks(n))}_addCallbacks(e){return e.map((e=>(e.leave=this._options.leave,e.hover=(t,n)=>{if(this._options.hover){const{range:o}=e;this._options.hover(t,n,o)}},e)))}};class o{static computeLink(e,t,r,i){const s=new RegExp(t.source,(t.flags||"")+"g"),[a,c]=o._getWindowedLineStrings(e-1,r),l=a.join("");let d;const p=[];for(;d=s.exec(l);){const e=d[0];if(!n(e))continue;const[t,s]=o._mapStrIdx(r,c,0,d.index),[a,l]=o._mapStrIdx(r,t,s,e.length);if(-1===t||-1===s||-1===a||-1===l)continue;const h={start:{x:s+1,y:t+1},end:{x:l,y:a+1}};p.push({range:h,text:e,activate:i})}return p}static _getWindowedLineStrings(e,t){let n,o=e,r=e,i=0,s="";const a=[];if(n=t.buffer.active.getLine(e)){const e=n.translateToString(!0);if(n.isWrapped&&" "!==e[0]){for(i=0;(n=t.buffer.active.getLine(--o))&&i<2048&&(s=n.translateToString(!0),i+=s.length,a.push(s),n.isWrapped&&-1===s.indexOf(" ")););a.reverse()}for(a.push(e),i=0;(n=t.buffer.active.getLine(++r))&&n.isWrapped&&i<2048&&(s=n.translateToString(!0),i+=s.length,a.push(s),-1===s.indexOf(" ")););}return[a,o]}static _mapStrIdx(e,t,n,o){const r=e.buffer.active,i=r.getNullCell();let s=n;for(;o;){const e=r.getLine(t);if(!e)return[-1,-1];for(let n=s;n<e.length;++n){e.getCell(n,i);const s=i.getChars();if(i.getWidth()&&(o-=s.length||1,n===e.length-1&&""===s)){const e=r.getLine(t+1);e&&e.isWrapped&&(e.getCell(0,i),2===i.getWidth()&&(o+=1))}if(o<0)return[t,n]}t++,s=0}return[t,s]}}t.LinkComputer=o}},t={};function n(o){var r=t[o];if(void 0!==r)return r.exports;var i=t[o]={exports:{}};return e[o](i,i.exports,n),i.exports}var o={};return(()=>{var e=o;Object.defineProperty(e,"__esModule",{value:!0}),e.WebLinksAddon=void 0;const t=n(6),r=/(https?|HTTPS?):[/]{2}[^\s"'!*(){}|\\\^<>`]*[^\s"':,.!?{}|\\\^~\[\]`()<>]/;function i(e,t){const n=window.open();if(n){try{n.opener=null}catch{}n.location.href=t}else console.warn("Opening link blocked as opener could not be cleared")}e.WebLinksAddon=class{constructor(e=i,t={}){this._handler=e,this._options=t}activate(e){this._terminal=e;const n=this._options,o=n.urlRegex||r;this._linkProvider=this._terminal.registerLinkProvider(new t.WebLinkProvider(this._terminal,o,this._handler,n))}dispose(){this._linkProvider?.dispose()}}})(),o})()));
//# sourceMappingURL=addon-web-links.js.map

View file

@ -36,6 +36,7 @@
*/
.xterm {
cursor: text;
position: relative;
user-select: none;
-ms-user-select: none;
@ -124,10 +125,6 @@
line-height: normal;
}
.xterm {
cursor: text;
}
.xterm.enable-mouse-events {
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
cursor: default;
@ -143,7 +140,7 @@
cursor: crosshair;
}
.xterm .xterm-accessibility,
.xterm .xterm-accessibility:not(.debug),
.xterm .xterm-message {
position: absolute;
left: 0;
@ -152,6 +149,16 @@
right: 0;
z-index: 10;
color: transparent;
pointer-events: none;
}
.xterm .xterm-accessibility-tree:not(.debug) *::selection {
color: transparent;
}
.xterm .xterm-accessibility-tree {
user-select: text;
white-space: pre;
}
.xterm .live-region {
@ -163,13 +170,49 @@
}
.xterm-dim {
opacity: 0.5;
/* Dim should not apply to background, so the opacity of the foreground color is applied
* explicitly in the generated class and reset to 1 here */
opacity: 1 !important;
}
.xterm-underline {
text-decoration: underline;
.xterm-underline-1 { text-decoration: underline; }
.xterm-underline-2 { text-decoration: double underline; }
.xterm-underline-3 { text-decoration: wavy underline; }
.xterm-underline-4 { text-decoration: dotted underline; }
.xterm-underline-5 { text-decoration: dashed underline; }
.xterm-overline {
text-decoration: overline;
}
.xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }
.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }
.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; }
.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }
.xterm-strikethrough {
text-decoration: line-through;
}
.xterm-screen .xterm-decoration-container .xterm-decoration {
z-index: 6;
position: absolute;
}
.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
z-index: 7;
}
.xterm-decoration-overview-ruler {
z-index: 8;
position: absolute;
top: 0;
right: 0;
pointer-events: none;
}
.xterm-decoration-top {
z-index: 2;
position: relative;
}

File diff suppressed because one or more lines are too long