Compare commits
76 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
239376dcef | ||
![]() |
16714160aa | ||
![]() |
52dcded70b | ||
![]() |
fb4117d844 | ||
![]() |
d635622d6f | ||
![]() |
74fbbc2209 | ||
![]() |
3e42875cb9 | ||
![]() |
8d473a0225 | ||
![]() |
2222951621 | ||
![]() |
aee895cabb | ||
![]() |
78be06afce | ||
![]() |
ce162c9e8c | ||
![]() |
d0e6d0e9ea | ||
![]() |
9a5f77d4b7 | ||
![]() |
cdd095e776 | ||
![]() |
cd75685862 | ||
![]() |
b33e3a4356 | ||
![]() |
004ea4f264 | ||
![]() |
00b5a3f42b | ||
![]() |
34f2a564db | ||
![]() |
46ffec65e2 | ||
![]() |
5716055716 | ||
![]() |
28f85baf59 | ||
![]() |
59740754e8 | ||
![]() |
529f720ae1 | ||
![]() |
71715e5040 | ||
![]() |
46f21f3a12 | ||
![]() |
f3cf5750ab | ||
![]() |
a874b2a332 | ||
![]() |
310a9ce2e2 | ||
![]() |
7bf9990995 | ||
![]() |
e28cf214df | ||
![]() |
9b723dcb8a | ||
![]() |
8055466b7a | ||
![]() |
aa1935e389 | ||
![]() |
51b63329ab | ||
![]() |
0a5ceaf9ea | ||
![]() |
236863a0b0 | ||
![]() |
4fc2577819 | ||
![]() |
88bea224d7 | ||
![]() |
3d56bc2182 | ||
![]() |
56b40dbef8 | ||
![]() |
517a5997fb | ||
![]() |
0f9381c00b | ||
![]() |
e01ffeb3db | ||
![]() |
a26b523053 | ||
![]() |
14621dbec2 | ||
![]() |
a3fc89faeb | ||
![]() |
20533a8c35 | ||
![]() |
32095987de | ||
![]() |
0069c378a7 | ||
![]() |
f64bebfe40 | ||
![]() |
9d841e48a4 | ||
![]() |
2f4a22a659 | ||
![]() |
26239de119 | ||
![]() |
98ab63f72c | ||
![]() |
43992f0864 | ||
![]() |
e0e2fca2a0 | ||
![]() |
86d4477e1c | ||
![]() |
e4b9b50072 | ||
![]() |
fcf626d03b | ||
![]() |
28afdc35ef | ||
![]() |
9858b64752 | ||
![]() |
cb1f2f7fc3 | ||
![]() |
307669f7c4 | ||
![]() |
0f30d2273a | ||
![]() |
b1956d3af8 | ||
![]() |
98a0c2a47b | ||
![]() |
d4db6f8e16 | ||
![]() |
208cfa8e0d | ||
![]() |
d28c611806 | ||
![]() |
9962e2ce43 | ||
![]() |
8adc03ac8f | ||
![]() |
97fb17dfe5 | ||
![]() |
96805eca37 | ||
![]() |
ba68b6fe02 |
|
@ -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:
|
||||
|
|
5
.github/workflows/deploy.yml
vendored
|
@ -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
|
||||
|
|
47
README.md
|
@ -5,7 +5,7 @@
|
|||
|
||||
This repository hosts the source code for [https://webvm.io](https://webvm.io), a Linux virtual machine that runs in your browser.
|
||||
|
||||
<img src="assets/welcome_to_WebVM_slim.png" width="95%">
|
||||
<img src="/assets/welcome_to_WebVM_2024.png" width="70%">
|
||||
|
||||
WebVM is a server-less virtual environment running fully client-side in HTML5/WebAssembly. It's designed to be Linux ABI-compatible. It runs an unmodified Debian distribution including many native development toolchains.
|
||||
|
||||
|
@ -13,8 +13,12 @@ WebVM is powered by the CheerpX virtualization engine, and enables safe, sandbox
|
|||
|
||||
# Enable networking
|
||||
|
||||
- Click "Connect via Tailscale" in the page header.
|
||||
- Log in to Tailscale (create an account if you don't have one).
|
||||
Modern browsers do not provide APIs to directly use TCP or UDP. WebVM provides networking support by integrating with Tailscale, a VPN network that supports WebSockets as a transport layer.
|
||||
|
||||
- Open the "Networking" panel from the side-bar
|
||||
- Click "Connect to Tailscale" from the panel
|
||||
- Log in to Tailscale (create an account if you don't have one)
|
||||
- Click "Connect" when prompted by Tailscale
|
||||
- If you are unfamiliar with Tailscale or would like additional information see [WebVM and Tailscale](/docs/Tailscale.md).
|
||||
|
||||
# Fork, deploy, customize
|
||||
|
@ -37,21 +41,24 @@ WebVM is powered by the CheerpX virtualization engine, and enables safe, sandbox
|
|||
|
||||
<img src="/assets/result.png" width="70%" >
|
||||
|
||||
You can now customize `dockerfiles/debian_mini` to suits your needs, or make a new Dockerfile from scratch. Use the `Path to Dockerfile` workflow parameter to select it.
|
||||
You can now customize `dockerfiles/debian_mini` to suit your needs, or make a new Dockerfile from scratch. Use the `Path to Dockerfile` workflow parameter to select it.
|
||||
|
||||
# Local deployment
|
||||
|
||||
From a local `git clone`
|
||||
|
||||
- Download the `debian_mini` Ext2 image from [https://github.com/leaningtech/webvm/releases/](https://github.com/leaningtech/webvm/releases/).
|
||||
- You can also build your own by selecting the "Upload GitHub release" workflow option.
|
||||
- Place the image in the repository root folder.
|
||||
- Edit `index.html`.
|
||||
- Uncomment the default values for `CMD`, `ARGS`, `ENV` and `CWD`.
|
||||
- Replace `DEVICE_TYPE` with `"bytes"`.
|
||||
- Replace `IMAGE_URL` with the name of the Ext2 image. For example `"debian_mini_20230519_5022088024.ext2"`.
|
||||
- Start a local HTTP server.
|
||||
- Enjoy your local WebVM.
|
||||
- Download the `debian_mini` Ext2 image from [https://github.com/leaningtech/webvm/releases/](https://github.com/leaningtech/webvm/releases/)
|
||||
- You can also build your own by selecting the "Upload GitHub release" workflow option
|
||||
- Place the image in the repository root folder
|
||||
- Edit `config_github_terminal.js`
|
||||
- Uncomment the default values for `CMD`, `ARGS`, `ENV` and `CWD`
|
||||
- Replace `IMAGE_URL` with the URL (absolute or relative) for the Ext2 image. For example `"/debian_mini_20230519_5022088024.ext2"`
|
||||
- Build WebVM using `npm`, output will be placed in the `build` directory
|
||||
- `npm install`
|
||||
- `npm run build`
|
||||
- Start NGINX, it automatically points to the `build` directory just created
|
||||
- `nginx -p . -c nginx.conf`
|
||||
- Visit `http://127.0.0.1:8081` and enjoy your local WebVM
|
||||
|
||||
# Example customization: Python3 REPL
|
||||
|
||||
|
@ -85,18 +92,18 @@ Or come to say hello / share your feedback on [Discord](https://discord.gg/yTNZg
|
|||
|
||||
# Thanks to...
|
||||
This project depends on:
|
||||
- [CheerpX](https://labs.leaningtech.com/cheerpx), made by [Leaning Technologies](https://leaningtech.com) for x86 virtualization and Linux emulation
|
||||
- [CheerpX](https://cheerpx.io/), made by [Leaning Technologies](https://leaningtech.com/) for x86 virtualization and Linux emulation
|
||||
- xterm.js, [https://xtermjs.org/](https://xtermjs.org/), for providing the Web-based terminal emulator
|
||||
- [Tailscale](https://tailscale.com/), for the networking component
|
||||
- [lwIP](https://savannah.nongnu.org/projects/lwip/), for the TCP/IP stack, compiled for the Web via [Cheerp](https://github.com/leaningtech/cheerp-meta)
|
||||
- [lwIP](https://savannah.nongnu.org/projects/lwip/), for the TCP/IP stack, compiled for the Web via [Cheerp](https://github.com/leaningtech/cheerp-meta/)
|
||||
|
||||
# Versioning
|
||||
|
||||
WebVM depends on the CheerpX x86-to-WebAssembly virtualization technology. A link to the current latest build is always available at [https://cheerpxdemos.leaningtech.com/publicdeploy/LATEST.txt](https://cheerpxdemos.leaningtech.com/publicdeploy/LATEST.txt). Builds of CheerpX are immutable and uniquely versioned. An example link would be:
|
||||
WebVM depends on the CheerpX x86-to-WebAssembly virtualization technology, which is included in the project via [NPM](https://www.npmjs.com/package/@leaningtech/cheerpx).
|
||||
|
||||
`https://cheerpxdemos.leaningtech.com/publicdeploy/20230517_94/cx.js`
|
||||
The NPM package is updated on every release.
|
||||
|
||||
We strongly encourage users _not_ to use the latest build. Please directly use a specific build to avoid unexpected regressions. Since builds are immutable, if they work for you now they will keep working forever.
|
||||
Every build is immutable, if a specific version works well for you today, it will keep working forever.
|
||||
|
||||
# License
|
||||
|
||||
|
@ -104,6 +111,8 @@ WebVM is released under the Apache License, Version 2.0.
|
|||
|
||||
You are welcome to use, modify, and redistribute the contents of this repository.
|
||||
|
||||
The public CheerpX deployment is provided **as-is** and is **free to use** for technological exploration, testing and non-commercial uses. Downloading a CheerpX build for the purpose of hosting it elsewhere is not permitted.
|
||||
The public CheerpX deployment is provided **as-is** and is **free to use** for technological exploration, testing and use by individuals. Any other use by organizations, including non-profit, academia and the public sector, requires a license. Downloading a CheerpX build for the purpose of hosting it elsewhere is not permitted without a commercial license.
|
||||
|
||||
Read more about [CheerpX licensing](https://cheerpx.io/docs/licensing)
|
||||
|
||||
If you want to build a product on top of CheerpX/WebVM, please get in touch: sales@leaningtech.com
|
||||
|
|
301
alpine.html
|
@ -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&display=swap&ver=6.0.2" media="all">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-fork-ribbon-css/0.2.3/gh-fork-ribbon.min.css" />
|
||||
<link rel="stylesheet" href="./xterm/xterm.css" />
|
||||
<link rel="stylesheet" href="./scrollbar.css" />
|
||||
<style>.github-fork-ribbon:before { background-color: #ea1e69; }</style>
|
||||
<!-- Serviceworker script that adds the COI and CORS headers to the response headers in cases where the server does not support it. -->
|
||||
<script src="serviceWorker.js"></script>
|
||||
<script src="./xterm/xterm.js"></script>
|
||||
<script src="./xterm/xterm-addon-fit.js"></script>
|
||||
<script src="./xterm/xterm-addon-web-links.js"></script>
|
||||
<script src="network.js"></script>
|
||||
</head>
|
||||
|
||||
<body style="margin:0;height:100%;background:black;color:white;overflow:hidden; display:flex; flex-direction: column; justify-content: space-between; height: 100%;">
|
||||
<a class="github-fork-ribbon right-bottom" href="https://github.com/leaningtech/webvm/" target="_blank" data-ribbon="Fork me on GitHub" title="Fork me on GitHub">Fork me on GitHub</a>
|
||||
<div>
|
||||
<div style="padding-top: 0.7em;padding-bottom: 0.7em;font-size: 0.3em; font-weight: 200;vertical-align:center;height: 120px;">
|
||||
<div style="margin-left: 20px; height: 100%; display: flex; align-items: center; justify-content: space-between;">
|
||||
<pre style="font-family: monospace; font-weight: 600; font-size: large; color: #ad7fa8;">
|
||||
|
||||
__ __ _ __ ____ __
|
||||
\ \ / /__| |_\ \ / / \/ |
|
||||
\ \/\/ / -_) '_ \ V /| |\/| |
|
||||
\_/\_/\___|_.__/\_/ |_| |_|
|
||||
</pre>
|
||||
<div style="height:100%;display: flex; flex-direction: column;justify-content: space-between;">
|
||||
<div style="padding-top: 0.7em;font-size: 0.3em; font-weight: 200;vertical-align:center;height:50px;">
|
||||
<div style="margin-right: 10px; margin-left: 20px; height: 100%; display: flex; align-items: center; justify-content: flex-end;gap: 50px;">
|
||||
<div style="padding-top: 0.7em;font-size: 0.3em; font-weight: 200;vertical-align:center;">
|
||||
<a href="https://leaningtech.com" style="text-decoration: none; height: 100%;" target="_blank">
|
||||
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
|
||||
<span>Made with ❤️ by </span>
|
||||
<img src="assets/leaningtech.png" height="40px" style="margin-left: 5px;">
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding-top: 0.7em;font-size: 0.3em; font-weight: 200;vertical-align:center;height:50px;">
|
||||
<div style="margin-right: 10px; margin-left: 20px; height: 100%; display: flex; align-items: center; justify-content: flex-end;gap: 50px;">
|
||||
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
|
||||
<span>CPU </span>
|
||||
<span id="cpuactivity" style="margin-left: 7px;">🟢</span>
|
||||
</div>
|
||||
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
|
||||
<span>HDD </span>
|
||||
<span id="hddactivity" style="margin-left: 7px;">🟢</span>
|
||||
</div>
|
||||
<a id="loginLink" style="user-select: text ;text-decoration: none; height: 100%;">
|
||||
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
|
||||
<div style="position: relative;">
|
||||
<span style="cursor: pointer" id="networkStatus">Connect via Tailscale </span>
|
||||
<span style="cursor: pointer; position: absolute; right: 0px; visibility: hidden;" id="ipCopied">Copied! </span>
|
||||
</div>
|
||||
<img src="assets/tailscale.svg" height="35px" style="margin-left: 7px;">
|
||||
</div>
|
||||
</a>
|
||||
<a href="https://discord.gg/yTNZgySKGa" style="text-decoration: none; height: 100%;" target="_blank">
|
||||
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
|
||||
<span>Join Discord </span>
|
||||
<img src="assets/discord-mark-blue.svg" height="35px" style="margin-left: 7px;">
|
||||
</div>
|
||||
</a>
|
||||
<a href="https://github.com/leaningtech/webvm/issues" style="text-decoration: none; height: 100%;" target="_blank">
|
||||
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
|
||||
<span>GitHub Issues </span>
|
||||
<img src="assets/github-mark-white.svg" height="35px" style="margin-left: 5px;">
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex-grow:0; flex-shrink: 0; height:1px; width: 100%; background-color: white;">
|
||||
</div>
|
||||
<main style="display: flex; flex-direction: row; justify-content: space-between; margin: 5px; height: 100%;">
|
||||
<div style="flex-grow:1; height:100%;display:inline-block;margin:0;" class="scrollbar" id="console">
|
||||
</div>
|
||||
<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
After Width: | Height: | Size: 82 KiB |
BIN
assets/social_2024.png
Normal file
After Width: | Height: | Size: 185 KiB |
BIN
assets/webvm_hero.png
Normal file
After Width: | Height: | Size: 514 KiB |
Before Width: | Height: | Size: 70 KiB |
BIN
assets/welcome_to_WebVM_2024.png
Normal file
After Width: | Height: | Size: 248 KiB |
Before Width: | Height: | Size: 47 KiB |
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
**When all set:**
|
||||
- Log in with your Tailscale credentials.
|
||||
- Go back to the WebVM tab.
|
||||
- `Connect via Tailscale` should be replaced by your IP address.
|
||||
- The `Connect to Tailscale` button in the Networking side-panel should be replaced by your IP address.
|
||||
|
||||
# Log in to Tailscale with an Auth key
|
||||
|
||||
|
|
BIN
documents/ArchitectureOverview.png
Normal file
After Width: | Height: | Size: 359 KiB |
BIN
documents/WebAssemblyTools.pdf
Normal file
5
documents/Welcome.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
Welcome to WebVM: A complete desktop environment running in the browser
|
||||
|
||||
WebVM is powered by CheerpX: a x86-to-WebAssembly virtualization engine and Just-in-Time compiler
|
||||
|
||||
For more info: https://cheerpx.io
|
3
documents/index.list
Normal file
|
@ -0,0 +1,3 @@
|
|||
ArchitectureOverview.png
|
||||
WebAssemblyTools.pdf
|
||||
Welcome.txt
|
414
index.html
|
@ -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&display=swap&ver=6.0.2" media="all">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-fork-ribbon-css/0.2.3/gh-fork-ribbon.min.css" />
|
||||
<link rel="stylesheet" href="./xterm/xterm.css" />
|
||||
<link rel="stylesheet" href="./scrollbar.css" />
|
||||
<style>.github-fork-ribbon:before { background-color: #ea1e69; }</style>
|
||||
<!-- Serviceworker script that adds the COI and CORS headers to the response headers in cases where the server does not support it. -->
|
||||
<script src="serviceWorker.js"></script>
|
||||
<script src="./xterm/xterm.js"></script>
|
||||
<script src="./xterm/xterm-addon-fit.js"></script>
|
||||
<script src="./xterm/xterm-addon-web-links.js"></script>
|
||||
<script src="network.js"></script>
|
||||
<script defer data-domain="webvm.io" src="https://plausible.leaningtech.com/js/script.js"></script>
|
||||
</head>
|
||||
|
||||
<body style="margin:0;height:100%;background:black;color:white;overflow:hidden; display:flex; flex-direction: column; justify-content: space-between; height: 100%;">
|
||||
<a class="github-fork-ribbon right-bottom" href="https://github.com/leaningtech/webvm/" target="_blank" data-ribbon="Fork me on GitHub" title="Fork me on GitHub">Fork me on GitHub</a>
|
||||
<div>
|
||||
<div style="padding-top: 0.7em;padding-bottom: 0.7em;font-size: 0.3em; font-weight: 200;vertical-align:center;height: 120px;">
|
||||
<div style="margin-left: 20px; height: 100%; display: flex; align-items: center; justify-content: space-between;">
|
||||
<pre style="font-family: monospace; font-weight: 600; font-size: large; color: #ad7fa8;">
|
||||
|
||||
__ __ _ __ ____ __
|
||||
\ \ / /__| |_\ \ / / \/ |
|
||||
\ \/\/ / -_) '_ \ V /| |\/| |
|
||||
\_/\_/\___|_.__/\_/ |_| |_|
|
||||
</pre>
|
||||
<div style="height:100%;display: flex; flex-direction: column;justify-content: space-between;">
|
||||
<div style="padding-top: 0.7em;font-size: 0.3em; font-weight: 200;vertical-align:center;height:50px;">
|
||||
<div style="margin-right: 10px; margin-left: 20px; height: 100%; display: flex; align-items: center; justify-content: flex-end;gap: 50px;">
|
||||
<div style="padding-top: 0.7em;font-size: 0.3em; font-weight: 200;vertical-align:center;">
|
||||
<a href="https://leaningtech.com" style="text-decoration: none; height: 100%;" target="_blank">
|
||||
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
|
||||
<span>Made with ❤️ by </span>
|
||||
<img src="assets/leaningtech.png" height="40px" style="margin-left: 5px;">
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding-top: 0.7em;font-size: 0.3em; font-weight: 200;vertical-align:center;height:50px;">
|
||||
<div style="margin-right: 10px; margin-left: 20px; height: 100%; display: flex; align-items: center; justify-content: flex-end;gap: 50px;">
|
||||
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
|
||||
<span>CPU </span>
|
||||
<span id="cpuactivity" style="margin-left: 7px;">🟢</span>
|
||||
</div>
|
||||
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
|
||||
<span>HDD </span>
|
||||
<span id="hddactivity" style="margin-left: 7px;">🟢</span>
|
||||
</div>
|
||||
<a id="loginLink" style="user-select: text ;text-decoration: none; height: 100%;">
|
||||
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
|
||||
<div style="position: relative;">
|
||||
<span style="cursor: pointer" id="networkStatus">Connect via Tailscale </span>
|
||||
<span style="cursor: pointer; position: absolute; right: 0px; visibility: hidden;" id="ipCopied">Copied! </span>
|
||||
</div>
|
||||
<img src="assets/tailscale.svg" height="35px" style="margin-left: 7px;">
|
||||
</div>
|
||||
</a>
|
||||
<a href="https://discord.gg/yTNZgySKGa" style="text-decoration: none; height: 100%;" target="_blank">
|
||||
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
|
||||
<span>Join Discord </span>
|
||||
<img src="assets/discord-mark-blue.svg" height="35px" style="margin-left: 7px;">
|
||||
</div>
|
||||
</a>
|
||||
<a href="https://github.com/leaningtech/webvm/issues" style="text-decoration: none; height: 100%;" target="_blank">
|
||||
<div style="color: white; font-family: montserrat; font-weight: 400; font-size: large; height: 100%; display: flex; align-items: center;">
|
||||
<span>GitHub Issues </span>
|
||||
<img src="assets/github-mark-white.svg" height="35px" style="margin-left: 5px;">
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex-grow:0; flex-shrink: 0; height:1px; width: 100%; background-color: white;">
|
||||
</div>
|
||||
<main style="display: flex; flex-direction: row; justify-content: space-between; margin: 5px; height: 100%;">
|
||||
<div style="flex-grow:1; height:100%;display:inline-block;margin:0;" class="scrollbar" id="console">
|
||||
</div>
|
||||
|
||||
</main>
|
||||
<script>
|
||||
|
||||
//Utility namespace to group all functionality related to printing (both error and non error) messages
|
||||
const color= "\x1b[1;35m";
|
||||
const bold= "\x1b[1;37m";
|
||||
const underline= "\x1b[94;4m";
|
||||
const normal= "\x1b[0m";
|
||||
var printOnTerm = {
|
||||
getAsciiText: function ()
|
||||
{
|
||||
var text = [
|
||||
"+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+",
|
||||
"| |",
|
||||
"| WebVM is a server-less virtual Linux environment running fully client-side |",
|
||||
"| in HTML5/WebAssembly. |",
|
||||
"| |",
|
||||
"| WebVM is powered by the CheerpX virtualization engine, which enables safe, |",
|
||||
"| sandboxed client-side execution of x86 binaries on any browser. |",
|
||||
"| |",
|
||||
"| CheerpX includes an x86-to-WebAssembly JIT compiler, a virtual block-based |",
|
||||
"| file system, and a Linux syscall emulator. |",
|
||||
"| |",
|
||||
"| 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>
|
||||
|
92
network.js
|
@ -1,92 +0,0 @@
|
|||
function setupNetworkInterface()
|
||||
{
|
||||
let params = new URLSearchParams("?"+window.location.hash.substr(1));
|
||||
let authKey = params.get("authKey") || undefined;
|
||||
let controlUrl = params.get("controlUrl") || undefined;
|
||||
console.log(authKey, controlUrl);
|
||||
let loginElemUrl = controlUrl ? null : "https://login.tailscale.com/admin/machines";
|
||||
|
||||
let resolveLogin = null;
|
||||
let loginPromise = new Promise((f,r) => {
|
||||
resolveLogin = f;
|
||||
});
|
||||
const loginElem = document.getElementById("loginLink");
|
||||
const statusElem = document.getElementById("networkStatus");
|
||||
const ipCopiedElem = document.getElementById("ipCopied");
|
||||
const loginUrlCb = (url) => {
|
||||
loginElem.href = url;
|
||||
loginElem.target = "_blank";
|
||||
statusElem.innerHTML = "Tailscale Login";
|
||||
resolveLogin(url);
|
||||
};
|
||||
const stateUpdateCb = (state) => {
|
||||
switch(state)
|
||||
{
|
||||
case 6 /*Running*/:
|
||||
{
|
||||
if (loginElemUrl) {
|
||||
loginElem.href = loginElemUrl;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
const netmapUpdateCb = (map) => {
|
||||
const ip = map.self.addresses[0];
|
||||
statusElem.innerText = "IP: "+ip;
|
||||
loginElem.title = "Right click to copy"
|
||||
const rmb_to_copy = (event) => {
|
||||
// To prevent the default contexmenu from showing up when right-clicking..
|
||||
event.preventDefault();
|
||||
// Copy the IP to the clipboard.
|
||||
window.navigator.clipboard.writeText(ip)
|
||||
.catch((msg) => { console.log("network.js: Copy ip to clipboard: Error: " + msg) });
|
||||
statusElem.style.visibility = "hidden";
|
||||
ipCopiedElem.style.visibility = "unset";
|
||||
setTimeout(() => {
|
||||
statusElem.style.visibility = "unset";
|
||||
ipCopiedElem.style.visibility = "hidden";
|
||||
}, 2000);
|
||||
};
|
||||
loginElem.addEventListener("contextmenu", rmb_to_copy);
|
||||
};
|
||||
loginElem.style.cursor = "pointer";
|
||||
loginElem.title = "Connect to Tailscale";
|
||||
statusElem.style.color = "white";
|
||||
return {
|
||||
loginUrlCb,
|
||||
stateUpdateCb,
|
||||
netmapUpdateCb,
|
||||
authKey,
|
||||
controlUrl,
|
||||
|
||||
loginElem,
|
||||
statusElem,
|
||||
loginElemUrl,
|
||||
loginPromise,
|
||||
};
|
||||
}
|
||||
function registerNetworkLogin(cx, { authKey, statusElem, loginElem, loginElemUrl, loginPromise })
|
||||
{
|
||||
if (authKey) {
|
||||
if (loginElemUrl) {
|
||||
loginElem.href = loginElemUrl;
|
||||
loginElem.target = "_blank";
|
||||
}
|
||||
cx.networkLogin();
|
||||
} else {
|
||||
loginElem.onclick = () => {
|
||||
loginElem.onclick = null;
|
||||
statusElem.innerHTML = "Downloading network code...";
|
||||
const w = window.open("login.html", "_blank");
|
||||
async function waitLogin() {
|
||||
await cx.networkLogin();
|
||||
statusElem.innerHTML = "Starting login...";
|
||||
const url = await loginPromise;
|
||||
statusElem.innerHTML = "Login URL ready...";
|
||||
w.location.href = url;
|
||||
}
|
||||
waitLogin();
|
||||
};
|
||||
}
|
||||
}
|
41
next.html
|
@ -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>
|
|
@ -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
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
18
src/app.html
|
@ -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
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
11
src/lib/InformationTab.svelte
Normal file
|
@ -0,0 +1,11 @@
|
|||
<h1 class="text-lg font-bold">Information</h1>
|
||||
<img src="assets/webvm_hero.png" alt="WebVM Logo" class="w-56 h-56 object-contain self-center">
|
||||
<p>WebVM is a virtual Linux environment running in the browser via WebAssembly</p>
|
||||
<p>It is based on:</p>
|
||||
<ul class="list-disc list-inside">
|
||||
<li><a class="underline" target="_blank" href="https://cheerpx.io/">CheerpX</a>: x86 JIT in Wasm</li>
|
||||
<li><a class="underline" target="_blank" href="https://xtermjs.org/">Xterm.js</a>: interactive terminal</li>
|
||||
<li>Local/private <a class="underline" target="_blank" href="https://cheerpx.io/docs/guides/File-System-support">file storage</a></li>
|
||||
<li><a class="underline" target="_blank" href="https://cheerpx.io/docs/guides/Networking">Networking</a> via <a class="underline" target="_blank" href="https://tailscale.com/">Tailscale</a></li>
|
||||
</ul>
|
||||
<slot></slot>
|
|
@ -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>
|
||||
|
|
|
@ -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
|
@ -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>
|
|
@ -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'}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -16,3 +16,11 @@ html
|
|||
{
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media (width <= 850px)
|
||||
{
|
||||
html
|
||||
{
|
||||
font-size: calc(100vw / 55);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:",
|
||||
""
|
||||
];
|
||||
|
|
|
@ -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 }
|
||||
|
|
44
src/routes/+layout.server.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { parse } from 'node-html-parser';
|
||||
import { read } from '$app/server';
|
||||
|
||||
var posts = [
|
||||
"https://labs.leaningtech.com/blog/webvm-20",
|
||||
"https://labs.leaningtech.com/blog/join-the-webvm-hackathon",
|
||||
"https://labs.leaningtech.com/blog/mini-webvm-your-linux-box-from-dockerfile-via-wasm",
|
||||
"https://labs.leaningtech.com/blog/webvm-virtual-machine-with-networking-via-tailscale",
|
||||
"https://labs.leaningtech.com/blog/webvm-server-less-x86-virtual-machines-in-the-browser",
|
||||
];
|
||||
|
||||
async function getPostData(u)
|
||||
{
|
||||
var ret = { title: null, image: null, url: u };
|
||||
var response = await fetch(u);
|
||||
var str = await response.text();
|
||||
var root = parse(str);
|
||||
var tags = root.getElementsByTagName("meta");
|
||||
for(var i=0;i<tags.length;i++)
|
||||
{
|
||||
var metaName = tags[i].getAttribute("property");
|
||||
var metaContent = tags[i].getAttribute("content");
|
||||
switch(metaName)
|
||||
{
|
||||
case "og:title":
|
||||
ret.title = metaContent;
|
||||
break;
|
||||
case "og:image":
|
||||
ret.image = metaContent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
export async function load()
|
||||
{
|
||||
var ret = [];
|
||||
for(var i=0;i<posts.length;i++)
|
||||
{
|
||||
ret.push(await getPostData(posts[i]));
|
||||
}
|
||||
return { posts: ret };
|
||||
}
|
1
src/routes/+page.js
Normal file
|
@ -0,0 +1 @@
|
|||
export const prerender = true;
|
|
@ -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>
|
||||
|
|
1
src/routes/alpine/+page.js
Normal file
|
@ -0,0 +1 @@
|
|||
export const prerender = true;
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
};
|
||||
|
|
|
@ -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: '' }
|
||||
]
|
||||
})
|
||||
]
|
||||
|
|