Browse Source

dev: add emulator page

KernelDeimos 8 months ago
parent
commit
dd8fe8f03e

+ 8 - 0
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'),

+ 19 - 0
src/emulator/assets/template.html

@@ -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>

+ 0 - 2
src/emulator/image/Dockerfile

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

+ 2 - 0
src/emulator/image/assets/.gitignore

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

+ 1 - 6
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]"

+ 2 - 0
src/emulator/image/build/.gitignore

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

+ 387 - 1
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();
+}

+ 43 - 0
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 -