dev: add emulator page

This commit is contained in:
KernelDeimos 2024-09-05 01:30:57 -04:00
parent 41e7526372
commit 28adc4e5d5
8 changed files with 462 additions and 9 deletions

View File

@ -117,10 +117,18 @@ class SelfHostedModule extends AdvancedBase {
prefix: '/builtin/dev-center',
path: path_.resolve(__dirname, RELATIVE_PATH, 'src/dev-center'),
},
{
prefix: '/builtin/emulator/image',
path: path_.resolve(__dirname, RELATIVE_PATH, 'src/emulator/image'),
},
{
prefix: '/builtin/emulator',
path: path_.resolve(__dirname, RELATIVE_PATH, 'src/emulator/dist'),
},
{
prefix: '/vendor/v86/bios',
path: path_.resolve(__dirname, RELATIVE_PATH, 'submodules/v86/bios'),
},
{
prefix: '/vendor/v86',
path: path_.resolve(__dirname, RELATIVE_PATH, 'submodules/v86/build'),

View File

@ -16,6 +16,20 @@
<script src="/puter.js/v2"></script>
<script src="/vendor/v86/libv86.js"></script>
<style>
div {
font-size: 12px;
line-height: 16px;
}
BODY {
background-color: #111;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
overflow: hidden;
}
</style>
</head>
<body>
@ -41,5 +55,10 @@
<script src="<%= htmlWebpackPlugin.files.chunks[chunk].entry %>"></script>
<% } %>
<div id="screen_container">
<div style="white-space: pre; font: 14px monospace; line-height: 14px"></div>
<canvas style="display: none"></canvas>
</div>
</body>
</html>

View File

@ -52,6 +52,4 @@ RUN rc-update add savecache shutdown
COPY rootfs/ /
RUN setup-hostname puter-alpine
RUN bash

2
src/emulator/image/assets/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -10,7 +10,7 @@ else
fi
IMAGES="$(dirname "$0")"/build/x86images
IMAGES="$(dirname "$0")"/build
OUT_ROOTFS_TAR="$IMAGES"/rootfs.tar
OUT_ROOTFS_BIN="$IMAGES"/rootfs.bin
OUT_ROOTFS_MNT="$IMAGES"/rootfs.mntpoint
@ -43,8 +43,3 @@ rm -rf "$OUT_ROOTFS_MNT"
echo "done! created"
sudo chown -R $USER:$USER $IMAGES/boot
cd "$IMAGES"
mkdir -p rootfs
split -b50M rootfs.bin rootfs/
cd ../
find x86images/rootfs/* | jq -Rnc "[inputs]"

2
src/emulator/image/build/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -1 +1,387 @@
puter.ui.launchApp('editor');
"use strict";
// puter.ui.launchApp('editor');
// Libs
// SO: 40031688
function buf2hex(buffer) { // buffer is an ArrayBuffer
return [...new Uint8Array(buffer)]
.map(x => x.toString(16).padStart(2, '0'))
.join('');
}
class ATStream {
constructor ({ delegate, acc, transform, observe }) {
this.delegate = delegate;
if ( acc ) this.acc = acc;
if ( transform ) this.transform = transform;
if ( observe ) this.observe = observe;
this.state = {};
this.carry = [];
}
[Symbol.asyncIterator]() { return this; }
async next_value_ () {
if ( this.carry.length > 0 ) {
console.log('got from carry!', this.carry);
return {
value: this.carry.shift(),
done: false,
};
}
return await this.delegate.next();
}
async acc ({ value }) {
return value;
}
async next_ () {
for (;;) {
const ret = await this.next_value_();
if ( ret.done ) return ret;
const v = await this.acc({
state: this.state,
value: ret.value,
carry: v => this.carry.push(v),
});
if ( this.carry.length >= 0 && v === undefined ) {
throw new Error(`no value, but carry value exists`);
}
if ( v === undefined ) continue;
// We have a value, clear the state!
this.state = {};
if ( this.transform ) {
const new_value = await this.transform(
{ value: ret.value });
return { ...ret, value: new_value };
}
return { ...ret, value: v };
}
}
async next () {
const ret = await this.next_();
if ( this.observe && !ret.done ) {
this.observe(ret);
}
return ret;
}
async enqueue_ (v) {
this.queue.push(v);
}
}
const NewCallbackByteStream = () => {
let listener;
let queue = [];
const NOOP = () => {};
let signal = NOOP;
(async () => {
for (;;) {
const v = await new Promise((rslv, rjct) => {
listener = rslv;
});
queue.push(v);
signal();
}
})();
const stream = {
[Symbol.asyncIterator](){
return this;
},
async next () {
if ( queue.length > 0 ) {
return {
value: queue.shift(),
done: false,
};
}
await new Promise(rslv => {
signal = rslv;
});
signal = NOOP;
const v = queue.shift();
return { value: v, done: false };
}
};
stream.listener = data => {
listener(data);
};
return stream;
}
// Tiny inline little-endian integer library
const get_int = (n_bytes, array8, signed=false) => {
return (v => signed ? v : v >>> 0)(
array8.slice(0,n_bytes).reduce((v,e,i)=>v|=e<<8*i,0));
}
const to_int = (n_bytes, num) => {
return (new Uint8Array()).map((_,i)=>(num>>8*i)&0xFF);
}
const NewVirtioFrameStream = byteStream => {
return new ATStream({
delegate: byteStream,
async acc ({ value, carry }) {
if ( ! this.state.buffer ) {
const size = get_int(4, value);
// 512MiB limit in case of attempted abuse or a bug
// (assuming this won't happen under normal conditions)
if ( size > 512*(1024**2) ) {
throw new Error(`Way too much data! (${size} bytes)`);
}
value = value.slice(4);
this.state.buffer = new Uint8Array(size);
this.state.index = 0;
}
const needed = this.state.buffer.length - this.state.index;
if ( value.length > needed ) {
const remaining = value.slice(needed);
console.log('we got more bytes than we needed',
needed,
remaining,
value.length,
this.state.buffer.length,
this.state.index,
);
carry(remaining);
}
const amount = Math.min(value.length, needed);
const added = value.slice(0, amount);
this.state.buffer.set(added, this.state.index);
this.state.index += amount;
if ( this.state.index > this.state.buffer.length ) {
throw new Error('WUT');
}
if ( this.state.index == this.state.buffer.length ) {
return this.state.buffer;
}
}
});
};
const wisp_types = [
{
id: 3,
label: 'CONTINUE',
describe: ({ payload }) => {
return `buffer: ${get_int(4, payload)}B`;
},
getAttributes ({ payload }) {
return {
buffer_size: get_int(4, payload),
};
}
},
{
id: 5,
label: 'INFO',
describe: ({ payload }) => {
return `v${payload[0]}.${payload[1]} ` +
buf2hex(payload.slice(2));
},
getAttributes ({ payload }) {
return {
version_major: payload[0],
version_minor: payload[1],
extensions: payload.slice(2),
}
}
},
];
class WispPacket {
static SEND = Symbol('SEND');
static RECV = Symbol('RECV');
constructor ({ data, direction, extra }) {
this.direction = direction;
this.data_ = data;
this.extra = extra ?? {};
this.types_ = {
1: { label: 'CONNECT' },
2: { label: 'DATA' },
4: { label: 'CLOSE' },
};
for ( const item of wisp_types ) {
this.types_[item.id] = item;
}
}
get type () {
const i_ = this.data_[0];
return this.types_[i_];
}
get attributes () {
if ( ! this.type.getAttributes ) return {};
const attrs = {};
Object.assign(attrs, this.type.getAttributes({
payload: this.data_.slice(5),
}));
Object.assign(attrs, this.extra);
return attrs;
}
toVirtioFrame () {
const arry = new Uint8Array(this.data_.length + 4);
arry.set(to_int(4, this.data_.length), 0);
arry.set(this.data_, 4);
return arry;
}
describe () {
return this.type.label + '(' +
(this.type.describe?.({
payload: this.data_.slice(5),
}) ?? '?') + ')';
}
log () {
const arrow =
this.direction === this.constructor.SEND ? '->' :
this.direction === this.constructor.RECV ? '<-' :
'<>' ;
console.groupCollapsed(`WISP ${arrow} ${this.describe()}`);
const attrs = this.attributes;
for ( const k in attrs ) {
console.log(k, attrs[k]);
}
console.groupEnd();
}
reflect () {
const reflected = new WispPacket({
data: this.data_,
direction:
this.direction === this.constructor.SEND ?
this.constructor.RECV :
this.direction === this.constructor.RECV ?
this.constructor.SEND :
undefined,
extra: {
reflectedFrom: this,
}
});
return reflected;
}
}
for ( const item of wisp_types ) {
WispPacket[item.label] = item;
}
const NewWispPacketStream = frameStream => {
return new ATStream({
delegate: frameStream,
transform ({ value }) {
return new WispPacket({
data: value,
direction: WispPacket.RECV,
});
},
observe ({ value }) {
value.log();
}
});
}
class WispClient {
constructor ({
packetStream,
sendFn,
}) {
this.packetStream = packetStream;
this.sendFn = sendFn;
}
send (packet) {
packet.log();
this.sendFn(packet);
}
}
window.onload = async function()
{
const resp = await fetch(
'./image/build/rootfs.bin'
);
const arrayBuffer = await resp.arrayBuffer();
var emulator = window.emulator = new V86({
wasm_path: "/vendor/v86/v86.wasm",
memory_size: 512 * 1024 * 1024,
vga_memory_size: 2 * 1024 * 1024,
screen_container: document.getElementById("screen_container"),
bios: {
url: "/vendor/v86/bios/seabios.bin",
},
vga_bios: {
url: "/vendor/v86/bios/vgabios.bin",
},
initrd: {
url: './image/build/boot/initramfs-lts',
},
bzimage: {
url: './image/build/boot/vmlinuz-lts',
async: false
},
cmdline: 'rw root=/dev/sda init=/sbin/init rootfstype=ext4',
// cmdline: 'rw root=/dev/sda init=/bin/bash rootfstype=ext4',
// cmdline: "rw init=/sbin/init root=/dev/sda rootfstype=ext4",
// cmdline: "rw init=/sbin/init root=/dev/sda rootfstype=ext4 random.trust_cpu=on 8250.nr_uarts=10 spectre_v2=off pti=off mitigations=off",
// cdrom: {
// // url: "../images/al32-2024.07.10.iso",
// url: "./image/build/rootfs.bin",
// },
hda: {
buffer: arrayBuffer,
// url: './image/build/rootfs.bin',
async: true,
// size: 1073741824,
// size: 805306368,
},
// bzimage_initrd_from_filesystem: true,
autostart: true,
network_relay_url: "wisp://127.0.0.1:3000",
virtio_console: true,
});
const decoder = new TextDecoder();
const byteStream = NewCallbackByteStream();
emulator.add_listener('virtio-console0-output-bytes',
byteStream.listener);
const virtioStream = NewVirtioFrameStream(byteStream);
const wispStream = NewWispPacketStream(virtioStream);
class PTYManager {
constructor ({ client }) {
this.client = client;
}
init () {
this.run_();
}
async run_ () {
const handlers_ = {
[WispPacket.INFO.id]: ({ packet }) => {
// console.log('guess we doing info packets now', packet);
this.client.send(packet.reflect());
}
};
for await ( const packet of this.client.packetStream ) {
// console.log('what we got here?',
// packet.type,
// packet,
// );
handlers_[packet.type.id]?.({ packet });
}
}
}
const ptyMgr = new PTYManager({
client: new WispClient({
packetStream: wispStream,
sendFn: packet => {
emulator.bus.send(
"virtio-console0-input-bytes",
packet.toVirtioFrame(),
);
}
})
});
ptyMgr.init();
}

43
tools/build_v86.sh Executable file
View File

@ -0,0 +1,43 @@
#!/bin/bash
start_dir=$(pwd)
cleanup() {
cd "$start_dir"
}
trap cleanup ERR EXIT
set -e
echo -e "\x1B[36;1m<<< Adding Targets >>>\x1B[0m"
rustup target add wasm32-unknown-unknown
rustup target add i686-unknown-linux-gnu
echo -e "\x1B[36;1m<<< Building v86 >>>\x1B[0m"
cd submodules/v86
make all
cd -
echo -e "\x1B[36;1m<<< Building Twisp >>>\x1B[0m"
cd submodules/twisp
RUSTFLAGS="-C target-feature=+crt-static" cargo build \
--release \
--target i686-unknown-linux-gnu \
`# TODO: what are default features?` \
--no-default-features
echo -e "\x1B[36;1m<<< Preparing to Build Imag >>>\x1B[0m"
cd -
cp submodules/twisp/target/i686-unknown-linux-gnu/release/twisp \
src/emulator/image/assets/
echo -e "\x1B[36;1m<<< Building Image >>>\x1B[0m"
cd src/emulator/image
./clean.sh
./build.sh
cd -