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