Sfoglia il codice sorgente

dev: add emulator image build

KernelDeimos 8 mesi fa
parent
commit
2c0b8428c5

+ 412 - 0
src/emulator/basic.html

@@ -0,0 +1,412 @@
+<!doctype html>
+<title>Basic Emulator</title><!-- not BASIC! -->
+<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>
+
+<script src="../build/libv86.js"></script>
+<script>
+"use strict";
+
+// 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/x86images/rootfs.bin'
+    );
+    const arrayBuffer = await resp.arrayBuffer();
+    var emulator = window.emulator = new V86({
+        wasm_path: "../build/v86.wasm",
+        memory_size: 512 * 1024 * 1024,
+        vga_memory_size: 2 * 1024 * 1024,
+        screen_container: document.getElementById("screen_container"),
+        bios: {
+            url: "../bios/seabios.bin",
+        },
+        vga_bios: {
+            url: "../bios/vgabios.bin",
+        },
+        
+        initrd: {
+            url: './image/build/x86images/boot/initramfs-lts',
+        },
+        bzimage: {
+            url: './image/build/x86images/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/x86images/rootfs.bin",
+        // },
+        hda: {
+            buffer: arrayBuffer,
+            // url: './image/build/x86images/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();
+}
+</script>
+
+<!-- A minimal structure for the ScreenAdapter defined in browser/screen.js -->
+<div id="screen_container">
+    <div style="white-space: pre; font: 14px monospace; line-height: 14px"></div>
+    <canvas style="display: none"></canvas>
+</div>

+ 57 - 0
src/emulator/image/Dockerfile

@@ -0,0 +1,57 @@
+FROM i386/alpine:edge
+
+RUN  apk add --update \
+        alpine-base bash ncurses shadow curl \
+        linux-lts linux-firmware-none linux-headers \
+        gcc make gcompat musl-dev libx11-dev xinit \
+        bind-tools \
+        util-linux \
+        htop vim nano \
+    && \
+    setup-xorg-base xhost xterm xcalc xdotool xkill || true && \
+    setup-devd udev || true && \
+    touch /root/.Xdefaults && \
+    rm /etc/motd /etc/issue && \
+    passwd -d root && \
+    chsh -s /bin/bash
+
+RUN apk add neofetch
+    
+COPY basic-boot /etc/init.d/
+RUN chmod +x /etc/init.d/basic-boot
+
+COPY assets/twisp /bin/twisp
+RUN chmod u+x /bin/twisp
+COPY twisp-service /etc/init.d/
+RUN chmod +x /etc/init.d/twisp-service
+RUN rc-update add twisp-service default
+
+COPY debug-service /etc/init.d/
+RUN chmod +x /etc/init.d/debug-service
+RUN rc-update add debug-service default
+
+COPY initd/network-service /etc/init.d/
+RUN chmod +x /etc/init.d/network-service
+RUN rc-update add network-service default
+
+# setup init system
+# COPY rc.conf /etc/rc.conf
+RUN rc-update add dmesg sysinit
+RUN rc-update add basic-boot sysinit
+
+RUN rc-update add root boot
+RUN rc-update add localmount boot
+RUN rc-update add modules boot
+RUN rc-update add sysctl boot
+RUN rc-update add bootmisc boot
+RUN rc-update add syslog boot
+
+RUN rc-update add mount-ro shutdown
+RUN rc-update add killprocs shutdown
+RUN rc-update add savecache shutdown
+
+COPY rootfs/ /
+
+RUN setup-hostname puter-alpine
+
+RUN bash

+ 14 - 0
src/emulator/image/basic-boot

@@ -0,0 +1,14 @@
+#!/sbin/openrc-run
+
+description="Run Essential Boot Scripts"
+
+start() {
+  ebegin "Running Essential Boot Scripts"
+  mount / -o remount,rw
+  eend $?
+}
+
+stop() {
+  ebegin "Stopping Essential Boot Scripts"
+  eend $?
+}

+ 50 - 0
src/emulator/image/build.sh

@@ -0,0 +1,50 @@
+#!/usr/bin/env bash
+set -veu
+
+if [ -w /var/run/docker.sock ]
+then
+    echo true
+else 
+    echo "You aren't in the docker group, please run usermod -a -G docker $USER && newgrp docker"
+    exit 2
+fi
+
+
+IMAGES="$(dirname "$0")"/build/x86images
+OUT_ROOTFS_TAR="$IMAGES"/rootfs.tar
+OUT_ROOTFS_BIN="$IMAGES"/rootfs.bin
+OUT_ROOTFS_MNT="$IMAGES"/rootfs.mntpoint
+CONTAINER_NAME=alpine-full
+IMAGE_NAME=i386/alpine-full
+
+rm -rf $OUT_ROOTFS_BIN || :
+
+mkdir -p "$IMAGES"
+docker build . --platform linux/386 --rm --tag "$IMAGE_NAME"
+docker rm "$CONTAINER_NAME" || true
+docker create --platform linux/386 -t -i --name "$CONTAINER_NAME" "$IMAGE_NAME" bash
+
+docker export "$CONTAINER_NAME" > "$OUT_ROOTFS_TAR"
+dd if=/dev/zero "of=$OUT_ROOTFS_BIN" bs=512M count=2
+
+loop=$(sudo losetup -f)
+sudo losetup -P "$loop" "$OUT_ROOTFS_BIN"
+sudo mkfs.ext4 "$loop"
+mkdir -p "$OUT_ROOTFS_MNT"
+sudo mount "$loop" "$OUT_ROOTFS_MNT"
+
+sudo tar -xf "$OUT_ROOTFS_TAR" -C "$OUT_ROOTFS_MNT"
+sudo cp -r "$OUT_ROOTFS_MNT/boot" "$IMAGES/boot"
+
+sudo umount "$loop"
+sudo losetup -d "$loop"
+rm "$OUT_ROOTFS_TAR"
+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]"

+ 3 - 0
src/emulator/image/clean.sh

@@ -0,0 +1,3 @@
+sudo umount build/x86images/rootfs.mntpoint
+sudo rm -rf ./build
+

+ 19 - 0
src/emulator/image/debug-service

@@ -0,0 +1,19 @@
+#!/sbin/openrc-run
+
+description="Run debug init"
+
+depend() {
+    after twisp-service
+}
+
+start() {
+  ebegin "Running Debug Init"
+  echo " 🛠 bash will be on tty2"
+  setsid bash < /dev/tty2 > /dev/tty2 2>&1 &
+  eend $?
+}
+
+stop() {
+  ebegin "Stopping Debug Init"
+  eend $?
+}

+ 17 - 0
src/emulator/image/initd/network-service

@@ -0,0 +1,17 @@
+#!/sbin/openrc-run
+
+description="Run network setup"
+
+start() {
+  ebegin "Running network setup"
+  modprobe ne2k-pci
+  ifupdown ifup eth0
+  ip link set lo up
+  echo "nameserver 192.168.86.1" > /etc/resolv.conf
+  eend $?
+}
+
+stop() {
+  ebegin "Stopping network setup"
+  eend $?
+}

+ 7 - 0
src/emulator/image/qemu.sh

@@ -0,0 +1,7 @@
+qemu-system-i386 \
+    -kernel ./build/x86images/boot/vmlinuz-lts \
+    -initrd ./build/x86images/boot/initramfs-lts \
+    -append "rw root=/dev/sda console=ttyS0 init=/sbin/init rootfstype=ext4" \
+    -hda ./build/x86images/rootfs.bin \
+    -m 1024M \
+    -nographic

+ 1 - 0
src/emulator/image/rootfs/etc/hostname

@@ -0,0 +1 @@
+puter-alpine

+ 4 - 0
src/emulator/image/rootfs/etc/network/interfaces

@@ -0,0 +1,4 @@
+iface eth0 inet static
+    address 192.168.86.100
+    netmask 255.255.255.0
+    gateway 192.168.86.1

+ 1 - 0
src/emulator/image/rootfs/etc/resolv.conf

@@ -0,0 +1 @@
+nameserver 192.168.86.1

+ 25 - 0
src/emulator/image/twisp-service

@@ -0,0 +1,25 @@
+#!/sbin/openrc-run
+
+description="twisp daemon"
+command="/bin/twisp"
+command_args="--pty /dev/hvc0"
+pidfile="/var/run/twisp.pid"
+command_background="yes"
+start_stop_daemon_args="--background --make-pidfile"
+
+depend() {
+    need localmount
+    after bootmisc
+}
+
+start() {
+    ebegin "Starting ${description}"
+    start-stop-daemon --start --pidfile "${pidfile}" --background --exec ${command} -- ${command_args}
+    eend $?
+}
+
+stop() {
+    ebegin "Stopping ${description}"
+    start-stop-daemon --stop --pidfile "${pidfile}"
+    eend $?
+}