Compare commits

..

76 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
48 changed files with 950 additions and 1318 deletions

View file

@ -32,7 +32,7 @@ jobs:
- run:
name: Deploy webvm
command: |
rsync -avz -e "ssh -p ${SSH_PORT}" webvm/build/ leaningtech@${SSH_HOST}:/srv/web/webvm_next/
rsync -avz -e "ssh -p ${SSH_PORT}" webvm/build/ leaningtech@${SSH_HOST}:/srv/web/webvm/
workflows:
deploy:

View file

@ -190,7 +190,10 @@ jobs:
WEBVM_MODE=github npm run build
# Move required files for gh-pages deployment to the deployment directory $DEPLOY_DIR.
- run: sudo mv build $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

View file

@ -5,7 +5,7 @@
This repository hosts the source code for [https://webvm.io](https://webvm.io), a Linux virtual machine that runs in your browser.
<img src="assets/welcome_to_WebVM_slim.png" width="95%">
<img src="/assets/welcome_to_WebVM_2024.png" width="70%">
WebVM is a server-less virtual environment running fully client-side in HTML5/WebAssembly. It's designed to be Linux ABI-compatible. It runs an unmodified Debian distribution including many native development toolchains.
@ -13,8 +13,12 @@ WebVM is powered by the CheerpX virtualization engine, and enables safe, sandbox
# Enable networking
- Click "Connect via Tailscale" in the page header.
- Log in to Tailscale (create an account if you don't have one).
Modern browsers do not provide APIs to directly use TCP or UDP. WebVM provides networking support by integrating with Tailscale, a VPN network that supports WebSockets as a transport layer.
- Open the "Networking" panel from the side-bar
- Click "Connect to Tailscale" from the panel
- Log in to Tailscale (create an account if you don't have one)
- Click "Connect" when prompted by Tailscale
- If you are unfamiliar with Tailscale or would like additional information see [WebVM and Tailscale](/docs/Tailscale.md).
# Fork, deploy, customize
@ -37,21 +41,24 @@ WebVM is powered by the CheerpX virtualization engine, and enables safe, sandbox
<img src="/assets/result.png" width="70%" >
You can now customize `dockerfiles/debian_mini` to suits your needs, or make a new Dockerfile from scratch. Use the `Path to Dockerfile` workflow parameter to select it.
You can now customize `dockerfiles/debian_mini` to suit your needs, or make a new Dockerfile from scratch. Use the `Path to Dockerfile` workflow parameter to select it.
# Local deployment
From a local `git clone`
- Download the `debian_mini` Ext2 image from [https://github.com/leaningtech/webvm/releases/](https://github.com/leaningtech/webvm/releases/).
- You can also build your own by selecting the "Upload GitHub release" workflow option.
- Place the image in the repository root folder.
- Edit `index.html`.
- Uncomment the default values for `CMD`, `ARGS`, `ENV` and `CWD`.
- Replace `DEVICE_TYPE` with `"bytes"`.
- Replace `IMAGE_URL` with the name of the Ext2 image. For example `"debian_mini_20230519_5022088024.ext2"`.
- Start a local HTTP server.
- Enjoy your local WebVM.
- Download the `debian_mini` Ext2 image from [https://github.com/leaningtech/webvm/releases/](https://github.com/leaningtech/webvm/releases/)
- You can also build your own by selecting the "Upload GitHub release" workflow option
- Place the image in the repository root folder
- Edit `config_github_terminal.js`
- Uncomment the default values for `CMD`, `ARGS`, `ENV` and `CWD`
- Replace `IMAGE_URL` with the URL (absolute or relative) for the Ext2 image. For example `"/debian_mini_20230519_5022088024.ext2"`
- Build WebVM using `npm`, output will be placed in the `build` directory
- `npm install`
- `npm run build`
- Start NGINX, it automatically points to the `build` directory just created
- `nginx -p . -c nginx.conf`
- Visit `http://127.0.0.1:8081` and enjoy your local WebVM
# Example customization: Python3 REPL
@ -85,18 +92,18 @@ Or come to say hello / share your feedback on [Discord](https://discord.gg/yTNZg
# Thanks to...
This project depends on:
- [CheerpX](https://labs.leaningtech.com/cheerpx), made by [Leaning Technologies](https://leaningtech.com) for x86 virtualization and Linux emulation
- [CheerpX](https://cheerpx.io/), made by [Leaning Technologies](https://leaningtech.com/) for x86 virtualization and Linux emulation
- xterm.js, [https://xtermjs.org/](https://xtermjs.org/), for providing the Web-based terminal emulator
- [Tailscale](https://tailscale.com/), for the networking component
- [lwIP](https://savannah.nongnu.org/projects/lwip/), for the TCP/IP stack, compiled for the Web via [Cheerp](https://github.com/leaningtech/cheerp-meta)
- [lwIP](https://savannah.nongnu.org/projects/lwip/), for the TCP/IP stack, compiled for the Web via [Cheerp](https://github.com/leaningtech/cheerp-meta/)
# Versioning
WebVM depends on the CheerpX x86-to-WebAssembly virtualization technology. A link to the current latest build is always available at [https://cheerpxdemos.leaningtech.com/publicdeploy/LATEST.txt](https://cheerpxdemos.leaningtech.com/publicdeploy/LATEST.txt). Builds of CheerpX are immutable and uniquely versioned. An example link would be:
WebVM depends on the CheerpX x86-to-WebAssembly virtualization technology, which is included in the project via [NPM](https://www.npmjs.com/package/@leaningtech/cheerpx).
`https://cheerpxdemos.leaningtech.com/publicdeploy/20230517_94/cx.js`
The NPM package is updated on every release.
We strongly encourage users _not_ to use the latest build. Please directly use a specific build to avoid unexpected regressions. Since builds are immutable, if they work for you now they will keep working forever.
Every build is immutable, if a specific version works well for you today, it will keep working forever.
# License
@ -104,6 +111,8 @@ WebVM is released under the Apache License, Version 2.0.
You are welcome to use, modify, and redistribute the contents of this repository.
The public CheerpX deployment is provided **as-is** and is **free to use** for technological exploration, testing and non-commercial uses. Downloading a CheerpX build for the purpose of hosting it elsewhere is not permitted.
The public CheerpX deployment is provided **as-is** and is **free to use** for technological exploration, testing and use by individuals. Any other use by organizations, including non-profit, academia and the public sector, requires a license. Downloading a CheerpX build for the purpose of hosting it elsewhere is not permitted without a commercial license.
Read more about [CheerpX licensing](https://cheerpx.io/docs/licensing)
If you want to build a product on top of CheerpX/WebVM, please get in touch: sales@leaningtech.com

View file

@ -1,301 +0,0 @@
<!DOCTYPE html>
<html lang="en" style="height:100%;">
<meta property="og:image" content="https://webvm.io/assets/reddit.png"/>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=1100, initial-scale=1.0">
<title>WebVM - Linux virtualization in WebAssembly</title>
<meta name="description" content="Server-less virtual machine, networking included, running browser-side in HTML5/WebAssembly. Code in any programming language inside this Linux terminal.">
<meta name="keywords" content="WebVM, Virtual Machine, CheerpX, x86 virtualization, WebAssembly, Tailscale, JIT">
<meta property="og:title" content="WebVM - Linux virtualization in WebAssembly" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="WebVM"/>
<meta property="og:image" content="https://webvm.io/assets/social.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@leaningtech" />
<meta name="twitter:title" content="WebVM - Linux virtualization in WebAssembly" />
<meta name="twitter:description" content="Server-less virtual machine, networking included, running browser-side in HTML5/WebAssembly. Code in any programming language inside this Linux terminal.">
<meta name="twitter:image" content="https://webvm.io/assets/social.png" />
<!-- Apple iOS web clip compatibility tags -->
<meta name="application-name" content="WebVM" />
<meta name="apple-mobile-web-app-title" content="WebVM" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<link rel="shortcut icon" href="./tower.ico">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" id="us-fonts-css" href="https://fonts.googleapis.com/css?family=Montserrat%3A300%2C400%2C500%2C600%2C700&amp;display=swap&amp;ver=6.0.2" media="all">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-fork-ribbon-css/0.2.3/gh-fork-ribbon.min.css" />
<link rel="stylesheet" href="./xterm/xterm.css" />
<link rel="stylesheet" href="./scrollbar.css" />
<style>.github-fork-ribbon:before { background-color: #ea1e69; }</style>
<!-- Serviceworker script that adds the COI and CORS headers to the response headers in cases where the server does not support it. -->
<script src="serviceWorker.js"></script>
<script src="./xterm/xterm.js"></script>
<script src="./xterm/xterm-addon-fit.js"></script>
<script src="./xterm/xterm-addon-web-links.js"></script>
<script src="network.js"></script>
</head>
<body style="margin:0;height:100%;background:black;color:white;overflow:hidden; display:flex; flex-direction: column; justify-content: space-between; height: 100%;">
<a class="github-fork-ribbon right-bottom" href="https://github.com/leaningtech/webvm/" target="_blank" data-ribbon="Fork me on GitHub" title="Fork me on GitHub">Fork me on GitHub</a>
<div>
<div style="padding-top: 0.7em;padding-bottom: 0.7em;font-size: 0.3em; font-weight: 200;vertical-align:center;height: 120px;">
<div style="margin-left: 20px; height: 100%; display: flex; align-items: center; justify-content: space-between;">
<pre style="font-family: monospace; font-weight: 600; font-size: large; color: #ad7fa8;">
__ __ _ __ ____ __
\ \ / /__| |_\ \ / / \/ |
\ \/\/ / -_) '_ \ V /| |\/| |
\_/\_/\___|_.__/\_/ |_| |_|
</pre>
<div style="height:100%;display: flex; flex-direction: column;justify-content: space-between;">
<div style="padding-top: 0.7em;font-size: 0.3em; font-weight: 200;vertical-align:center;height:50px;">
<div style="margin-right: 10px; margin-left: 20px; height: 100%; display: flex; align-items: center; justify-content: flex-end;gap: 50px;">
<div style="padding-top: 0.7em;font-size: 0.3em; font-weight: 200;vertical-align:center;">
<a href="https://leaningtech.com" style="text-decoration: none; height: 100%;" target="_blank">
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
<span>Made with &#10084;&#65039; by </span>
<img src="assets/leaningtech.png" height="40px" style="margin-left: 5px;">
</div>
</a>
</div>
</div>
</div>
<div style="padding-top: 0.7em;font-size: 0.3em; font-weight: 200;vertical-align:center;height:50px;">
<div style="margin-right: 10px; margin-left: 20px; height: 100%; display: flex; align-items: center; justify-content: flex-end;gap: 50px;">
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
<span>CPU </span>
<span id="cpuactivity" style="margin-left: 7px;">&#x1F7E2;</span>
</div>
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
<span>HDD </span>
<span id="hddactivity" style="margin-left: 7px;">&#x1F7E2;</span>
</div>
<a id="loginLink" style="user-select: text ;text-decoration: none; height: 100%;">
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
<div style="position: relative;">
<span style="cursor: pointer" id="networkStatus">Connect via Tailscale </span>
<span style="cursor: pointer; position: absolute; right: 0px; visibility: hidden;" id="ipCopied">Copied! </span>
</div>
<img src="assets/tailscale.svg" height="35px" style="margin-left: 7px;">
</div>
</a>
<a href="https://discord.gg/yTNZgySKGa" style="text-decoration: none; height: 100%;" target="_blank">
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
<span>Join Discord </span>
<img src="assets/discord-mark-blue.svg" height="35px" style="margin-left: 7px;">
</div>
</a>
<a href="https://github.com/leaningtech/webvm/issues" style="text-decoration: none; height: 100%;" target="_blank">
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
<span>GitHub Issues </span>
<img src="assets/github-mark-white.svg" height="35px" style="margin-left: 5px;">
</div>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<div style="flex-grow:0; flex-shrink: 0; height:1px; width: 100%; background-color: white;">
</div>
<main style="display: flex; flex-direction: row; justify-content: space-between; margin: 5px; height: 100%;">
<div style="flex-grow:1; height:100%;display:inline-block;margin:0;" class="scrollbar" id="console">
</div>
<canvas id="canvas"></canvas>
</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 = {
getSharedArrayBufferMissingMessage: function ()
{
const isCustom = window.location.hostname !== "webvm.io";
const isSecureContext = window.isSecureContext;
const text = [
"",
"",
color + "CheerpX could not start" + normal,
"",
"CheerpX uses SharedArrayBuffer, which is not available right now.",
"",
!isSecureContext && " - This page is not in a secure context. Serve over HTTPS or WSS.",
!isSecureContext && " " + underline + "https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts" + normal,
isCustom && " - The document is not cross-origin isolated.",
isCustom && " " + underline + "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements" + normal,
" - Your browser might not support SharedArrayBuffer.",
" Since 2022, all major browsers support this feature.",
" " + underline + "https://caniuse.com/sharedarraybuffer" + normal,
].filter(Boolean);
return text;
},
getErrorMessage: function (error_message)
{
const text = [
"",
"",
color + "CheerpX could not start" + normal,
"",
"CheerpX internal error message is:",
error_message,
"",
"",
"CheerpX is expected to work with recent desktop versions of Chrome, Edge, Firefox and Safari",
"",
"",
"Give it a try from a desktop version / another browser!",
]
return text;
},
printMessage: function (text) {
for (var i=0; i<text.length; i++)
{
term.write(text[i]);
term.write('\n');
}
},
printError: function (message)
{
this.printMessage(message);
term.write("\n\n");
function writeCustom(something)
{
term.write(something);
}
},
};
var consoleDiv = document.getElementById("console");
//xterm.js related logic
var term = new Terminal({cursorBlink:true,convertEol:true, fontFamily:"monospace", fontWeight: 400, fontWeightBold: 700});
var fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
var linkAddon = new WebLinksAddon.WebLinksAddon();
term.loadAddon(linkAddon);
term.open(consoleDiv);
term.scrollToTop();
fitAddon.fit();
window.addEventListener("resize", function(ev){fitAddon.fit();}, false);
term.focus();
var cxReadFunc = null;
function writeData(buf, vt)
{
// Unsure why the output is not sent to vt 0
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));
}
term.onData(readData);
function hddCallback(state)
{
var h = document.getElementById("hddactivity");
if(state == "ready")
h.textContent = "\u{1F7E2}";
else
h.textContent = "\u{1F7E0}";
}
function cpuCallback(state)
{
var h = document.getElementById("cpuactivity");
if(state == "ready")
h.textContent = "\u{1F7E2}";
else
h.textContent = "\u{1F7E0}";
}
//Actual CheerpX and init specific logic
async function runInit()
{
if (typeof SharedArrayBuffer === "undefined")
{
printOnTerm.printError(printOnTerm.getSharedArrayBufferMissingMessage());
return;
}
let networkInterface = setupNetworkInterface();
async function runTest(cx)
{
cx.registerCallback("cpuActivity", cpuCallback);
cx.registerCallback("diskActivity", hddCallback);
registerNetworkLogin(cx, networkInterface);
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);
const canvas = document.getElementById("canvas");
cx.setKmsCanvas(canvas, 1024, 768);
var opts = {uid: 0, git: 0};
cx.run("/sbin/init", [], opts);
}
function failCallback(err)
{
printOnTerm.printError(printOnTerm.getErrorMessage(err));
}
var params = new URLSearchParams(location.search);
var protocol = params.get("setProtocol");
if(!protocol)
protocol = "https:";
var blockDevice = await CheerpX.CloudDevice.create(protocol + "//disks-staging.webvm.io/alpine_20240307_i3_slimmed.ext2");
var overlayDevice = await CheerpX.OverlayDevice.create(blockDevice, await CheerpX.IDBDevice.create("block2"));
var webDevice = await CheerpX.WebDevice.create("");
var dataDevice = await CheerpX.DataDevice.create();
CheerpX.Linux.create({mounts:[{type:"ext2",dev:overlayDevice,path:"/"},{type:"dir",dev:webDevice,path:"/app"},{type:"dir",dev:dataDevice,path:"/data"},{type:"devs",path:"/dev"},{type:"proc",path:"/proc"}], networkInterface: 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();
async function loadCX()
{
// Find the latest build
var r = await fetch("https://cheerpxdemos.leaningtech.com/publicdeploy/LATEST.txt");
var url = await r.text();
url = url.trim();
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.addEventListener("load", runInit, false);
document.head.appendChild(script);
}
loadCX();
</script>
</body>
</html>

BIN
assets/alpine_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
assets/social_2024.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

BIN
assets/webvm_hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

View file

@ -7,15 +7,15 @@ export const printIntro = true;
// Is a graphical display needed
export const needsDisplay = false;
// Executable full path (Required)
export const cmd = CMD;
export const cmd = CMD; // Default: "/bin/bash";
// Arguments, as an array (Required)
export const args = ARGS;
export const args = ARGS; // Default: ["--login"];
// Optional extra parameters
export const opts = {
// Environment variables
env: ENV,
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,
cwd: CWD, // Default: "/home/user",
// User id
uid: 1000,
// Group id

View file

@ -1,5 +1,5 @@
// The root filesystem location
export const diskImageUrl = "wss://disks.webvm.io/alpine_20240307_i3_slimmed.ext2";
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

View file

@ -7,7 +7,7 @@
**When all set:**
- Log in with your Tailscale credentials.
- Go back to the WebVM tab.
- `Connect via Tailscale` should be replaced by your IP address.
- The `Connect to Tailscale` button in the Networking side-panel should be replaced by your IP address.
# Log in to Tailscale with an Auth key

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 KiB

Binary file not shown.

5
documents/Welcome.txt Normal file
View file

@ -0,0 +1,5 @@
Welcome to WebVM: A complete desktop environment running in the browser
WebVM is powered by CheerpX: a x86-to-WebAssembly virtualization engine and Just-in-Time compiler
For more info: https://cheerpx.io

3
documents/index.list Normal file
View file

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

View file

@ -1,414 +0,0 @@
<!DOCTYPE html>
<html lang="en" style="height:100%;">
<meta property="og:image" content="https://webvm.io/assets/reddit.png"/>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=1100, initial-scale=1.0">
<title>WebVM - Linux virtualization in WebAssembly</title>
<meta name="description" content="Server-less virtual machine, networking included, running browser-side in HTML5/WebAssembly. Code in any programming language inside this Linux terminal.">
<meta name="keywords" content="WebVM, Virtual Machine, CheerpX, x86 virtualization, WebAssembly, Tailscale, JIT">
<meta property="og:title" content="WebVM - Linux virtualization in WebAssembly" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="WebVM"/>
<meta property="og:image" content="https://webvm.io/assets/social.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@leaningtech" />
<meta name="twitter:title" content="WebVM - Linux virtualization in WebAssembly" />
<meta name="twitter:description" content="Server-less virtual machine, networking included, running browser-side in HTML5/WebAssembly. Code in any programming language inside this Linux terminal.">
<meta name="twitter:image" content="https://webvm.io/assets/social.png" />
<!-- Apple iOS web clip compatibility tags -->
<meta name="application-name" content="WebVM" />
<meta name="apple-mobile-web-app-title" content="WebVM" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<link rel="shortcut icon" href="./tower.ico">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" id="us-fonts-css" href="https://fonts.googleapis.com/css?family=Montserrat%3A300%2C400%2C500%2C600%2C700&amp;display=swap&amp;ver=6.0.2" media="all">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-fork-ribbon-css/0.2.3/gh-fork-ribbon.min.css" />
<link rel="stylesheet" href="./xterm/xterm.css" />
<link rel="stylesheet" href="./scrollbar.css" />
<style>.github-fork-ribbon:before { background-color: #ea1e69; }</style>
<!-- Serviceworker script that adds the COI and CORS headers to the response headers in cases where the server does not support it. -->
<script src="serviceWorker.js"></script>
<script src="./xterm/xterm.js"></script>
<script src="./xterm/xterm-addon-fit.js"></script>
<script src="./xterm/xterm-addon-web-links.js"></script>
<script src="network.js"></script>
<script defer data-domain="webvm.io" src="https://plausible.leaningtech.com/js/script.js"></script>
</head>
<body style="margin:0;height:100%;background:black;color:white;overflow:hidden; display:flex; flex-direction: column; justify-content: space-between; height: 100%;">
<a class="github-fork-ribbon right-bottom" href="https://github.com/leaningtech/webvm/" target="_blank" data-ribbon="Fork me on GitHub" title="Fork me on GitHub">Fork me on GitHub</a>
<div>
<div style="padding-top: 0.7em;padding-bottom: 0.7em;font-size: 0.3em; font-weight: 200;vertical-align:center;height: 120px;">
<div style="margin-left: 20px; height: 100%; display: flex; align-items: center; justify-content: space-between;">
<pre style="font-family: monospace; font-weight: 600; font-size: large; color: #ad7fa8;">
__ __ _ __ ____ __
\ \ / /__| |_\ \ / / \/ |
\ \/\/ / -_) '_ \ V /| |\/| |
\_/\_/\___|_.__/\_/ |_| |_|
</pre>
<div style="height:100%;display: flex; flex-direction: column;justify-content: space-between;">
<div style="padding-top: 0.7em;font-size: 0.3em; font-weight: 200;vertical-align:center;height:50px;">
<div style="margin-right: 10px; margin-left: 20px; height: 100%; display: flex; align-items: center; justify-content: flex-end;gap: 50px;">
<div style="padding-top: 0.7em;font-size: 0.3em; font-weight: 200;vertical-align:center;">
<a href="https://leaningtech.com" style="text-decoration: none; height: 100%;" target="_blank">
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
<span>Made with &#10084;&#65039; by </span>
<img src="assets/leaningtech.png" height="40px" style="margin-left: 5px;">
</div>
</a>
</div>
</div>
</div>
<div style="padding-top: 0.7em;font-size: 0.3em; font-weight: 200;vertical-align:center;height:50px;">
<div style="margin-right: 10px; margin-left: 20px; height: 100%; display: flex; align-items: center; justify-content: flex-end;gap: 50px;">
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
<span>CPU </span>
<span id="cpuactivity" style="margin-left: 7px;">&#x1F7E2;</span>
</div>
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
<span>HDD </span>
<span id="hddactivity" style="margin-left: 7px;">&#x1F7E2;</span>
</div>
<a id="loginLink" style="user-select: text ;text-decoration: none; height: 100%;">
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
<div style="position: relative;">
<span style="cursor: pointer" id="networkStatus">Connect via Tailscale </span>
<span style="cursor: pointer; position: absolute; right: 0px; visibility: hidden;" id="ipCopied">Copied! </span>
</div>
<img src="assets/tailscale.svg" height="35px" style="margin-left: 7px;">
</div>
</a>
<a href="https://discord.gg/yTNZgySKGa" style="text-decoration: none; height: 100%;" target="_blank">
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
<span>Join Discord </span>
<img src="assets/discord-mark-blue.svg" height="35px" style="margin-left: 7px;">
</div>
</a>
<a href="https://github.com/leaningtech/webvm/issues" style="text-decoration: none; height: 100%;" target="_blank">
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
<span>GitHub Issues </span>
<img src="assets/github-mark-white.svg" height="35px" style="margin-left: 5px;">
</div>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<div style="flex-grow:0; flex-shrink: 0; height:1px; width: 100%; background-color: white;">
</div>
<main style="display: flex; flex-direction: row; justify-content: space-between; margin: 5px; height: 100%;">
<div style="flex-grow:1; height:100%;display:inline-block;margin:0;" class="scrollbar" id="console">
</div>
</main>
<script>
//Utility namespace to group all functionality related to printing (both error and non error) messages
const color= "\x1b[1;35m";
const bold= "\x1b[1;37m";
const underline= "\x1b[94;4m";
const normal= "\x1b[0m";
var printOnTerm = {
getAsciiText: function ()
{
var text = [
"+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+",
"| |",
"| WebVM is a server-less virtual Linux environment running fully client-side |",
"| in HTML5/WebAssembly. |",
"| |",
"| WebVM is powered by the CheerpX virtualization engine, which enables safe, |",
"| sandboxed client-side execution of x86 binaries on any browser. |",
"| |",
"| CheerpX includes an x86-to-WebAssembly JIT compiler, a virtual block-based |",
"| file system, and a Linux syscall emulator. |",
"| |",
"| Your own WebVM with custom images via Dockerfile: |",
"| |",
"| " + underline + "https://leaningtech.com/mini-webvm-your-linux-box-from-dockerfile-via-wasm" + normal +" |",
"| |",
"| Join WebVM: The Hackathon (11-14 October 2024) |",
"| |",
"| " + underline + "https://cheerpx.io/hackathon" + normal + " |",
"| |",
"+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+",
"",
" Welcome to WebVM. If unsure, try these examples:",
"",
" python3 examples/python3/fibonacci.py ",
" gcc -o helloworld examples/c/helloworld.c && ./helloworld",
" objdump -d ./helloworld | less -M",
" vim examples/c/helloworld.c",
" curl --max-time 15 parrot.live # requires networking",
"",
];
return text;
},
getSharedArrayBufferMissingMessage: function ()
{
const isCustom = window.location.hostname !== "webvm.io";
const isSecureContext = window.isSecureContext;
const text = [
"",
"",
color + "CheerpX could not start" + normal,
"",
"CheerpX uses SharedArrayBuffer, which is not available right now.",
"",
!isSecureContext && " - This page is not in a secure context. Serve over HTTPS or WSS.",
!isSecureContext && " " + underline + "https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts" + normal,
isCustom && " - The document is not cross-origin isolated.",
isCustom && " " + underline + "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements" + normal,
" - Your browser might not support SharedArrayBuffer.",
" Since 2022, all major browsers support this feature.",
" " + underline + "https://caniuse.com/sharedarraybuffer" + normal,
].filter(Boolean);
return text;
},
getErrorMessage: function (error_message)
{
const text = [
"",
"",
color + "CheerpX could not start" + normal,
"",
"CheerpX internal error message is:",
error_message,
"",
"",
"CheerpX is expected to work with recent desktop versions of Chrome, Edge, Firefox and Safari",
"",
"",
"Give it a try from a desktop version / another browser!",
]
return text;
},
printMessage: function (text) {
for (var i=0; i<text.length; i++)
{
term.write(text[i]);
term.write('\n');
}
},
printError: function (message)
{
this.printMessage(message);
term.write("\n\n");
function writeCustom(something)
{
term.write(something);
}
},
};
var consoleDiv = document.getElementById("console");
//xterm.js related logic
var term = new Terminal({cursorBlink:true,convertEol:true, fontFamily:"monospace", fontWeight: 400, fontWeightBold: 700});
var fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
var linkAddon = new WebLinksAddon.WebLinksAddon();
term.loadAddon(linkAddon);
term.open(consoleDiv);
term.scrollToTop();
fitAddon.fit();
window.addEventListener("resize", function(ev){fitAddon.fit();}, false);
term.focus();
var cxReadFunc = null;
function writeData(buf)
{
term.write(new Uint8Array(buf));
}
function readData(str)
{
if(cxReadFunc == null)
return;
for(var i=0;i<str.length;i++)
cxReadFunc(str.charCodeAt(i));
}
term.onData(readData);
function hddCallback(state)
{
var h = document.getElementById("hddactivity");
if(state == "ready")
h.textContent = "\u{1F7E2}";
else
h.textContent = "\u{1F7E0}";
}
function cpuCallback(state)
{
var h = document.getElementById("cpuactivity");
if(state == "ready")
h.textContent = "\u{1F7E2}";
else
h.textContent = "\u{1F7E0}";
}
//Actual CheerpX and bash specific logic
async function runBash()
{
// cmd, cwd, args and env are replaced by the Github actions workflow.
var cmd = CMD;
var args = ARGS;
var env = ENV;
var cwd = CWD;
var device_type = DEVICE_TYPE;
var image_url = IMAGE_URL;
// Reasonable defaults for local deployments
// var cmd = "/bin/bash";
// var args = ["--login"];
// var env = ["HOME=/home/user", "TERM=xterm", "USER=user", "SHELL=/bin/bash", "EDITOR=vim", "LANG=en_US.UTF-8", "LC_ALL=C"];
// var cwd = "/home/user";
// var device_type = "bytes";
// var image_url = "/your_local_image.ext2";
const structure = {
cmd: cmd,
args: args,
env: env,
cwd: cwd
}
if (typeof SharedArrayBuffer === "undefined")
{
printOnTerm.printError(printOnTerm.getSharedArrayBufferMissingMessage());
return;
}
let networkInterface = setupNetworkInterface();
async function runTest(cx)
{
var processCount = 0;
function handleProcessCreated()
{
processCount++;
plausible(`Process started: ${processCount}`);
if(processCount == 5)
{
// Make sure no further event is reported
cx.unregisterCallback("processCreated", handleProcessCreated);
}
}
cx.registerCallback("processCreated", handleProcessCreated);
cx.registerCallback("cpuActivity", cpuCallback);
cx.registerCallback("diskActivity", hddCallback);
registerNetworkLogin(cx, networkInterface);
term.scrollToBottom();
async function cxLogAndRun(cheerpx, cmd, args, env)
{
await cheerpx.run(cmd, args, env);
printOnTerm.printMessage(" ");
}
cxReadFunc = cx.setCustomConsole(writeData, term.cols, term.rows);
function preventDefaults (e) {
e.preventDefault()
e.stopPropagation()
}
consoleDiv.addEventListener("dragover", preventDefaults, false);
consoleDiv.addEventListener("dragenter", preventDefaults, false);
consoleDiv.addEventListener("dragleave", preventDefaults, false);
consoleDiv.addEventListener("drop", preventDefaults, false);
var opts = {env:structure.env, cwd:structure.cwd, uid: 1000, gid: 1000};
while (true)
{
await cxLogAndRun(cx, structure.cmd, structure.args, opts);
}
}
function failCallback(err)
{
printOnTerm.printError(printOnTerm.getErrorMessage(err));
}
// The device url and type are replaced by Github Actions.
var blockDevice;
switch (device_type)
{
case "cloud":
try
{
blockDevice = await CheerpX.CloudDevice.create(image_url);
}
catch(e)
{
// Report the failure and try again with plain HTTP
var wssProtocol = "wss:";
if(image_url.startsWith(wssProtocol))
{
// WebSocket protocol failed, try agin using plain HTTP
plausible("WS Disk failure");
image_url = "https:" + image_url.substr(wssProtocol.length);
blockDevice = await CheerpX.CloudDevice.create(image_url);
}
else
{
// No other recovery option
throw e;
}
}
break;
case "bytes":
try
{
blockDevice = await CheerpX.HttpBytesDevice.create(image_url);
}
catch(e)
{
printOnTerm.printError([e]);
throw e;
}
break;
case "github":
blockDevice = await CheerpX.GitHubDevice.create(image_url);
break;
default:
console.log("Unrecognized device type");
return;
}
var overlayDevice = await CheerpX.OverlayDevice.create(blockDevice, await CheerpX.IDBDevice.create("block1"));
var webDevice = await CheerpX.WebDevice.create("");
var dataDevice = await CheerpX.DataDevice.create();
CheerpX.Linux.create({mounts:[{type:"ext2",dev:overlayDevice,path:"/"},{type:"dir",dev:webDevice,path:"/app"},{type:"dir",dev:dataDevice,path:"/data"},{type:"devs",path:"/dev"},{type:"proc",path:"/proc"}], networkInterface: networkInterface}).then(runTest, failCallback);
}
function initialMessage()
{
printOnTerm.printMessage(printOnTerm.getAsciiText());
console.log("Welcome. We appreciate curiosity, but be warned that keeping the DevTools open causes significant performance degradation and crashes.");
}
initialMessage();
async function loadCX()
{
// Find the latest build
var r = await fetch("https://cxrtnc.leaningtech.com/LATEST.txt");
var url = await r.text();
url = url.trim();
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.addEventListener("load", runBash, false);
document.head.appendChild(script);
}
loadCX();
</script>
</body>
</html>

View file

@ -1,92 +0,0 @@
function setupNetworkInterface()
{
let params = new URLSearchParams("?"+window.location.hash.substr(1));
let authKey = params.get("authKey") || undefined;
let controlUrl = params.get("controlUrl") || undefined;
console.log(authKey, controlUrl);
let loginElemUrl = controlUrl ? null : "https://login.tailscale.com/admin/machines";
let resolveLogin = null;
let loginPromise = new Promise((f,r) => {
resolveLogin = f;
});
const loginElem = document.getElementById("loginLink");
const statusElem = document.getElementById("networkStatus");
const ipCopiedElem = document.getElementById("ipCopied");
const loginUrlCb = (url) => {
loginElem.href = url;
loginElem.target = "_blank";
statusElem.innerHTML = "Tailscale Login";
resolveLogin(url);
};
const stateUpdateCb = (state) => {
switch(state)
{
case 6 /*Running*/:
{
if (loginElemUrl) {
loginElem.href = loginElemUrl;
}
break;
}
}
};
const netmapUpdateCb = (map) => {
const ip = map.self.addresses[0];
statusElem.innerText = "IP: "+ip;
loginElem.title = "Right click to copy"
const rmb_to_copy = (event) => {
// To prevent the default contexmenu from showing up when right-clicking..
event.preventDefault();
// Copy the IP to the clipboard.
window.navigator.clipboard.writeText(ip)
.catch((msg) => { console.log("network.js: Copy ip to clipboard: Error: " + msg) });
statusElem.style.visibility = "hidden";
ipCopiedElem.style.visibility = "unset";
setTimeout(() => {
statusElem.style.visibility = "unset";
ipCopiedElem.style.visibility = "hidden";
}, 2000);
};
loginElem.addEventListener("contextmenu", rmb_to_copy);
};
loginElem.style.cursor = "pointer";
loginElem.title = "Connect to Tailscale";
statusElem.style.color = "white";
return {
loginUrlCb,
stateUpdateCb,
netmapUpdateCb,
authKey,
controlUrl,
loginElem,
statusElem,
loginElemUrl,
loginPromise,
};
}
function registerNetworkLogin(cx, { authKey, statusElem, loginElem, loginElemUrl, loginPromise })
{
if (authKey) {
if (loginElemUrl) {
loginElem.href = loginElemUrl;
loginElem.target = "_blank";
}
cx.networkLogin();
} else {
loginElem.onclick = () => {
loginElem.onclick = null;
statusElem.innerHTML = "Downloading network code...";
const w = window.open("login.html", "_blank");
async function waitLogin() {
await cx.networkLogin();
statusElem.innerHTML = "Starting login...";
const url = await loginPromise;
statusElem.innerHTML = "Login URL ready...";
w.location.href = url;
}
waitLogin();
};
}
}

View file

@ -1,41 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset='utf-8'>
<meta name="viewport" content="width=1100, initial-scale=1.0">
<title>WebVM - Linux virtualization in WebAssembly</title>
<meta name="description" content="Server-less virtual machine, networking included, running browser-side in HTML5/WebAssembly. Code in any programming language inside this Linux terminal.">
<meta name="keywords" content="WebVM, Virtual Machine, CheerpX, x86 virtualization, WebAssembly, Tailscale, JIT">
<meta property="og:title" content="WebVM - Linux virtualization in WebAssembly" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="WebVM"/>
<meta property="og:image" content="https://webvm.io/assets/social.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@leaningtech" />
<meta name="twitter:title" content="WebVM - Linux virtualization in WebAssembly" />
<meta name="twitter:description" content="Server-less virtual machine, networking included, running browser-side in HTML5/WebAssembly. Code in any programming language inside this Linux terminal.">
<meta name="twitter:image" content="https://webvm.io/assets/social.png" />
<!-- Apple iOS web clip compatibility tags -->
<meta name="application-name" content="WebVM" />
<meta name="apple-mobile-web-app-title" content="WebVM" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<link rel="shortcut icon" href="./tower.ico">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel='stylesheet' href='/build/bundle.css'>
<link rel='stylesheet' href='scrollbar.css'>
<!-- Serviceworker script that adds the COI and CORS headers to the response headers in cases where the server does not support it. -->
<script src="serviceWorker.js"></script>
<script src="network.js"></script>
<script defer src='/build/bundle.js' type='module'></script>
</head>
<body>
</body>
</html>

View file

@ -38,7 +38,6 @@ http {
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;
try_files $uri /index.html;
}
}
}

840
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "webvm",
"version": "1.0.0",
"version": "2.0.0",
"private": true,
"scripts": {
"dev": "vite dev",
@ -19,6 +19,7 @@
"@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",

View file

@ -14,6 +14,10 @@ export default {
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

@ -5,17 +5,17 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<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="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.png" />
<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="Server-less virtual machine, networking included, running browser-side in HTML5/WebAssembly. Code in any programming language inside this Linux terminal.">
<meta name="twitter:image" content="https://webvm.io/assets/social.png" />
<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" />
@ -23,13 +23,13 @@
<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="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 and CORS headers to the response headers in cases where the server does not support it. -->
<script src="/serviceWorker.js"></script>
<script defer data-domain="webvm.io" src="https://plausible.leaningtech.com/js/script.js"></script>
<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">

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>

View file

@ -1,4 +1,12 @@
<h1 class="text-lg font-bold">CPU</h1>
<p>WebVM is powered by CheerpX, a x86 virtualization engine in WebAssembly</p>
<p>CheerpX can run securely run unmodified x86 binaries and libraries in the browser</p>
<p>For more information: <a class="text-gray-300" href="https://cheerpx.io/" target="_blank">https://cheerpx.io</a></p>
<script>
import PanelButton from './PanelButton.svelte';
import { cpuPercentage } from './activities.js'
</script>
<h1 class="text-lg font-bold">Engine</h1>
<PanelButton buttonImage="assets/cheerpx.svg" clickUrl="https://cheerpx.io/docs" buttonText="Explore CheerpX">
</PanelButton>
<p><span class="font-bold">Virtual CPU: </span>{$cpuPercentage}%</p>
<p>CheerpX is a x86 virtualization engine in WebAssembly</p>
<p>It can securely run unmodified x86 binaries and libraries in the browser</p>
<p>Excited about our technology? <a class="underline" href="https://cheerpx.io/docs/getting-started" target="_blank">Start building</a> your projects using <a class="underline" href="https://cheerpx.io/" target="_blank">CheerpX</a> today!</p>

View file

@ -1,8 +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 buttonIcon="assets/discord-mark-blue.svg" clickUrl="https://discord.gg/yTNZgySKGa" buttonText="Join us on Discord"/>
<PanelButton buttonImage="assets/discord-mark-blue.svg" clickUrl="https://discord.gg/yTNZgySKGa" buttonText="Join our Discord">
<i class='fas fa-circle fa-xs ml-auto text-green-500'></i>
<span class="ml-1"><DiscordPresenceCount /></span>
</PanelButton>
<p>Do you have any question about WebVM or CheerpX?</p>
<p>Join our community, we are happy to help!</p>

View file

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

View file

@ -1,8 +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 buttonIcon="assets/github-mark-white.svg" clickUrl="https://github.com/leaningtech/webvm" buttonText="GitHub Repo"/>
<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 GitHub issue</p>
<p>Found a bug? Please open a <a class="underline" href="https://github.com/leaningtech/webvm/issues" target="_blank">GitHub issue</a></p>

View file

@ -12,7 +12,8 @@
</script>
<div
class="p-3 cursor-pointer text-center {$activity ? "text-lime-500" : "hover:text-gray-100"}"
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>

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

@ -4,6 +4,7 @@
import PanelButton from './PanelButton.svelte';
var dispatch = createEventDispatcher();
var connectionState = networkData.connectionState;
var exitNode = networkData.exitNode;
function handleConnect() {
connectionState.set("DOWNLOADING");
dispatch('connect');
@ -98,6 +99,10 @@
}
</script>
<h1 class="text-lg font-bold">Networking</h1>
<PanelButton buttonIcon="assets/tailscale.svg" clickUrl={getClickUrl($connectionState)} clickHandler={getClickHandler($connectionState)} rightClickHandler={getRightClickHandler($connectionState)} buttonTooltip={getButtonTooltip($connectionState)} buttonText={getButtonText($connectionState)}/>
<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

@ -3,8 +3,17 @@
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;
export let buttonIcon;
</script>
<a href={clickUrl} target="_blank" on:click={clickHandler} on:contextmenu={rightClickHandler}><p class="bg-neutral-700 p-2 rounded-md {(clickUrl != null || clickHandler != null) ? "hover:bg-neutral-500 cursor-pointer" : ""}" title={buttonTooltip}><img src={buttonIcon} class="inline w-8 h-8"/><span class="ml-1">{buttonText}</span></p></a>
<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>

View file

@ -1,18 +1,21 @@
<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-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 },
];
@ -44,12 +47,18 @@
{/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 === 'Networking'}
{#if activeInfo === 'Information'}
<InformationTab>
<slot></slot>
</InformationTab>
{:else if activeInfo === 'Networking'}
<NetworkingTab on:connect/>
{:else if activeInfo === 'CPU'}
<CpuTab/>
{:else if activeInfo === 'Disk'}
<DiskTab/>
<DiskTab on:reset/>
{:else if activeInfo === 'Posts'}
<PostsTab/>
{:else if activeInfo === 'Discord'}
<DiscordTab/>
{:else if activeInfo === 'GitHub'}

View file

@ -1,29 +1,28 @@
<script>
import { onMount } from 'svelte';
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import { WebLinksAddon } from '@xterm/addon-web-links';
import Nav from 'labs/packages/global-navbar/src/Nav.svelte';
import SideBar from '$lib/SideBar.svelte';
import * as CheerpX from '@leaningtech/cheerpx';
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 } from '$lib/activities.js'
import { introMessage, errorMessage } from '$lib/messages.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 = new Terminal({cursorBlink:true, convertEol:true, fontFamily:"monospace", fontWeight: 400, fontWeightBold: 700});
var term = null;
var cx = null;
var fitAddon = new FitAddon();
term.loadAddon(fitAddon);
var linkAddon = new WebLinksAddon();
term.loadAddon(linkAddon);
var fitAddon = null;
var cxReadFunc = null;
var blockCache = null;
var processCount = 0;
var curVT = 0;
function writeData(buf, vt)
{
if(vt != 1)
@ -42,23 +41,144 @@
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);
}
term.onData(readData);
function initTerminal()
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", function(ev){ fitAddon.fit(); });
window.addEventListener("resize", handleResize);
term.focus();
term.onData(readData);
// Avoid undesired default DnD handling
function preventDefaults (e) {
e.preventDefault()
@ -68,17 +188,32 @@
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);
initCheerpX();
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.style.zIndex = 10;
display.parentElement.style.zIndex = 5;
plausible("Display activated");
}
function handleProcessCreated()
{
@ -88,7 +223,7 @@
}
async function initCheerpX()
{
// TODO: Check for SAB support
const CheerpX = await import('@leaningtech/cheerpx');
var blockDevice = null;
switch(configObj.diskImageType)
{
@ -123,8 +258,10 @@
default:
throw new Error("Unrecognized device type");
}
var overlayDevice = await CheerpX.OverlayDevice.create(blockDevice, await CheerpX.IDBDevice.create("block1"));
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
@ -135,8 +272,12 @@
{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"}
{type:"proc", path:"/proc"},
// Convenient access to sample documents in the user directory
{type:"dir", dev:documentsDevice, path:"/home/user/documents"}
];
try
{
@ -150,13 +291,14 @@
}
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)
{
cx.setKmsCanvas(display, 1024, 768);
setScreenSize(display);
cx.setActivateConsole(handleActivateConsole);
}
// Run the command in a loop, in case the user exits
@ -172,14 +314,26 @@
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}/>
<SideBar on:connect={handleConnect} on:reset={handleReset}>
<slot></slot>
</SideBar>
{#if configObj.needsDisplay}
<canvas class="absolute top-0 bottom-0 left-14 right-0" width="1024" height="768" id="display"></canvas>
<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>

View file

@ -2,3 +2,5 @@ import { writable } from 'svelte/store';
export const cpuActivity = writable(false);
export const diskActivity = writable(false);
export const cpuPercentage = writable(0);
export const diskLatency = writable(0);

View file

@ -16,3 +16,11 @@ html
{
height: 100%;
}
@media (width <= 850px)
{
html
{
font-size: calc(100vw / 55);
}
}

View file

@ -4,18 +4,19 @@ const normal= "\x1b[0m";
export const introMessage = [
"+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+",
"| |",
"| WebVM is a server-less virtual Linux environment running fully client-side |",
"| in HTML5/WebAssembly. |",
"| 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 on any browser. |",
"| 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. |",
"| file system, and a Linux syscall emulator |",
"| |",
"| Your own WebVM with custom images via Dockerfile: |",
"| [News] WebVM 2.0: A complete Linux Desktop Environment in the browser: |",
"| |",
"| " + underline + "https://leaningtech.com/mini-webvm-your-linux-box-from-dockerfile-via-wasm" + normal +" |",
"| " + underline + "https://labs.leaningtech.com/blog/webvm-20" + normal + " |",
"| |",
"| Try out the new Alpine / Xorg / i3 WebVM: " + underline + "https://webvm.io/alpine.html" + normal + " |",
"| |",
"+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+",
"",
@ -38,3 +39,13 @@ export const errorMessage = [
"CheerpX internal error message is:",
""
];
export const unexpectedErrorMessage = [
color + "WebVM encountered an unexpected error" + normal,
"",
"Check the DevTools console for further information",
"",
"Please consider reporting a bug!",
"",
"CheerpX internal error message is:",
""
];

View file

@ -1,18 +1,26 @@
import { writable } from 'svelte/store';
import { browser } from '$app/environment'
let params = new URLSearchParams("?"+window.location.hash.substr(1));
let authKey = params.get("authKey") || undefined;
let controlUrl = params.get("controlUrl") || undefined;
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)
{
resolveLogin(url);
connectionState.set("LOGINREADY");
resolveLogin(url);
}
function stateUpdateCb(state)
@ -30,6 +38,19 @@ function stateUpdateCb(state)
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()
@ -37,10 +58,9 @@ export async function startLogin()
connectionState.set("LOGINSTARTING");
const url = await loginPromise;
networkData.loginUrl = url;
connectionState.set("LOGINREADY");
return url;
}
export const networkInterface = { authKey: authKey, controlUrl: controlUrl, loginUrlCb: loginUrlCb, stateUpdateCb: stateUpdateCb, netmapUpdateCb: netmapUpdateCb };
export const networkData = { currentIp: null, connectionState: connectionState, loginUrl: null, dashboardUrl: dashboardUrl }
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;

View file

@ -11,4 +11,6 @@ function handleProcessCreated(processCount)
}
</script>
<WebVM configObj={configObj} processCallback={handleProcessCreated} />
<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

@ -11,4 +11,6 @@ function handleProcessCreated(processCount)
}
</script>
<WebVM configObj={configObj} processCallback={handleProcessCreated} />
<WebVM configObj={configObj} processCallback={handleProcessCreated} cacheId="blocks_alpine">
<p>Looking for something different? Try the classic <a class="underline" href="/" target="_blank">Debian Linux</a> terminal-based WebVM</p>
</WebVM>

View file

@ -4,7 +4,7 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter({fallback: 'index.html'})
adapter: adapter()
},
preprocess: vitePreprocess()
};

View file

@ -5,7 +5,8 @@ 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'
'/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: {
@ -19,7 +20,8 @@ export default defineConfig({
{ src: 'scrollbar.css', dest: '' },
{ src: 'serviceWorker.js', dest: '' },
{ src: 'login.html', dest: '' },
{ src: 'assets/', dest: '' }
{ src: 'assets/', dest: '' },
{ src: 'documents/', dest: '' }
]
})
]