diff --git a/src/backend/src/modules/selfhosted/SelfHostedModule.js b/src/backend/src/modules/selfhosted/SelfHostedModule.js index 6ec4b6e2..a1098942 100644 --- a/src/backend/src/modules/selfhosted/SelfHostedModule.js +++ b/src/backend/src/modules/selfhosted/SelfHostedModule.js @@ -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'), diff --git a/src/emulator/assets/template.html b/src/emulator/assets/template.html index 87721584..15d555bc 100644 --- a/src/emulator/assets/template.html +++ b/src/emulator/assets/template.html @@ -16,6 +16,20 @@ + @@ -41,5 +55,10 @@ <% } %> +
+
+ +
+ \ No newline at end of file diff --git a/src/emulator/image/Dockerfile b/src/emulator/image/Dockerfile index c45e3b8e..d46fa597 100644 --- a/src/emulator/image/Dockerfile +++ b/src/emulator/image/Dockerfile @@ -52,6 +52,4 @@ RUN rc-update add savecache shutdown COPY rootfs/ / -RUN setup-hostname puter-alpine - RUN bash \ No newline at end of file diff --git a/src/emulator/image/assets/.gitignore b/src/emulator/image/assets/.gitignore new file mode 100644 index 00000000..d6b7ef32 --- /dev/null +++ b/src/emulator/image/assets/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/src/emulator/image/build.sh b/src/emulator/image/build.sh index f8d51fa5..8e3a45b6 100755 --- a/src/emulator/image/build.sh +++ b/src/emulator/image/build.sh @@ -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]" diff --git a/src/emulator/image/build/.gitignore b/src/emulator/image/build/.gitignore new file mode 100644 index 00000000..d6b7ef32 --- /dev/null +++ b/src/emulator/image/build/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/src/emulator/src/main.js b/src/emulator/src/main.js index e4d8630c..37d5a3c6 100644 --- a/src/emulator/src/main.js +++ b/src/emulator/src/main.js @@ -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(); +} diff --git a/tools/build_v86.sh b/tools/build_v86.sh new file mode 100755 index 00000000..d4746225 --- /dev/null +++ b/tools/build_v86.sh @@ -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 -