From fa77b597c90c4dab15f64c7e24086f8f21ab9915 Mon Sep 17 00:00:00 2001 From: Fabian Date: Mon, 23 Sep 2024 11:57:38 -0600 Subject: [PATCH 01/90] add test for reboot --- Makefile | 1 + tests/api/reboot.js | 61 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100755 tests/api/reboot.js diff --git a/Makefile b/Makefile index bba402d2a4..3c135daa70 100644 --- a/Makefile +++ b/Makefile @@ -322,6 +322,7 @@ api-tests: all-debug ./tests/api/reset.js ./tests/api/floppy-insert-eject.js ./tests/api/serial.js + ./tests/api/reboot.js all-tests: eslint kvm-unit-test qemutests qemutests-release jitpagingtests api-tests nasmtests nasmtests-force-jit tests expect-tests # Skipping: diff --git a/tests/api/reboot.js b/tests/api/reboot.js new file mode 100755 index 0000000000..1cf54eb872 --- /dev/null +++ b/tests/api/reboot.js @@ -0,0 +1,61 @@ +#!/usr/bin/env node +"use strict"; + +const TEST_RELEASE_BUILD = +process.env.TEST_RELEASE_BUILD; + +const fs = require("fs"); +var V86 = require(`../../build/${TEST_RELEASE_BUILD ? "libv86" : "libv86-debug"}.js`).V86; + +process.on("unhandledRejection", exn => { throw exn; }); + +const config = { + bios: { url: __dirname + "/../../bios/seabios.bin" }, + vga_bios: { url: __dirname + "/../../bios/vgabios.bin" }, + cdrom: { url: __dirname + "/../../images/linux4.iso", async: true }, + net_device: { + relay_url: "fetch", + type: "virtio", + }, + autostart: true, + memory_size: 32 * 1024 * 1024, + filesystem: {}, + virtio_console: true, + log_level: 0, + disable_jit: +process.env.DISABLE_JIT, +}; + +const emulator = new V86(config); + +let did_reboot = false; +let serial_text = ""; + +const timeout = setTimeout(() => { + console.log(serial_data); + throw new Error("Timeout"); +}, 120 * 1000); + +emulator.add_listener("serial0-output-byte", function(byte) +{ + var chr = String.fromCharCode(byte); + serial_text += chr; + + if(did_reboot) + { + if(serial_text.endsWith("Files send via emulator appear in /mnt/")) + { + console.log("Ok"); + emulator.stop(); + clearTimeout(timeout); + } + } + else + { + if(serial_text.endsWith("~% ")) + { + console.log("rebooting"); + emulator.serial0_send("reboot\n"); + serial_text = ""; + did_reboot = true; + } + } +}); From b7a54ba042c5f39e3a9cf90e4641fbbd2b665ab5 Mon Sep 17 00:00:00 2001 From: Fabian Date: Mon, 23 Sep 2024 11:58:24 -0600 Subject: [PATCH 02/90] fix #1073, again --- src/cpu.js | 6 +++--- src/virtio_net.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cpu.js b/src/cpu.js index c1b6210ed0..917fd1b2f0 100644 --- a/src/cpu.js +++ b/src/cpu.js @@ -689,15 +689,15 @@ CPU.prototype.reboot_internal = function() if(this.devices.virtio_9p) { - this.devices.virtio_9p.Reset(); + this.devices.virtio_9p.reset(); } if(this.devices.virtio_console) { - this.devices.virtio_console.Reset(); + this.devices.virtio_console.reset(); } if(this.devices.virtio_net) { - this.devices.virtio_net.Reset(); + this.devices.virtio_net.reset(); } this.load_bios(); diff --git a/src/virtio_net.js b/src/virtio_net.js index eb972986a3..29b4152468 100644 --- a/src/virtio_net.js +++ b/src/virtio_net.js @@ -229,8 +229,8 @@ VirtioNet.prototype.set_state = function(state) this.virtio.set_state(state[0]); }; -VirtioNet.prototype.Reset = function() { - +VirtioNet.prototype.reset = function() { + this.virtio.reset(); }; VirtioNet.prototype.Send = function (queue_id, bufchain, blob) From 5cf6a6591551e49c6bdae1bc3a3a6a974fb37b05 Mon Sep 17 00:00:00 2001 From: Fabian Date: Mon, 23 Sep 2024 12:16:04 -0600 Subject: [PATCH 03/90] fix bug in wait_until_vga_screen_contains (sometimes caused tests to hang) --- src/browser/starter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/starter.js b/src/browser/starter.js index 5813c1fa97..114e1f0898 100644 --- a/src/browser/starter.js +++ b/src/browser/starter.js @@ -1385,7 +1385,7 @@ V86.prototype.wait_until_vga_screen_contains = function(text) function put_char(args) { const [row, col, char] = args; - changed_rows.add(col); + changed_rows.add(row); } const check = () => From d49cb4c65a7a126e45f25141fad3181d79c49359 Mon Sep 17 00:00:00 2001 From: Fabian Date: Mon, 23 Sep 2024 12:16:17 -0600 Subject: [PATCH 04/90] make floppy test more robust --- tests/api/floppy-insert-eject.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/api/floppy-insert-eject.js b/tests/api/floppy-insert-eject.js index 12246c87ac..2f5aa98891 100755 --- a/tests/api/floppy-insert-eject.js +++ b/tests/api/floppy-insert-eject.js @@ -3,6 +3,7 @@ const TEST_RELEASE_BUILD = +process.env.TEST_RELEASE_BUILD; +const pause = require("timers/promises").setTimeout; const fs = require("fs"); var V86 = require(`../../build/${TEST_RELEASE_BUILD ? "libv86" : "libv86-debug"}.js`).V86; @@ -16,23 +17,34 @@ const emulator = new V86({ autostart: true, memory_size: 32 * 1024 * 1024, filesystem: {}, - log_level: 0, + log_level: 3, disable_jit: +process.env.DISABLE_JIT, }); +//const interval = setInterval(() => { +// console.warn(emulator.screen_adapter.get_text_screen()); +//}, 1000); + const timeout = setTimeout(() => { + console.warn(emulator.screen_adapter.get_text_screen()); throw new Error("Timeout"); }, 60 * 1000); setTimeout(async () => { await emulator.wait_until_vga_screen_contains("C:\\> "); + console.log("Got C:\\>"); + await pause(1000); emulator.keyboard_send_text("dir A:\n"); await emulator.wait_until_vga_screen_contains("Abort, Retry, Fail?"); + console.log("Got Abort, Retry, Fail?"); + await pause(1000); emulator.keyboard_send_text("F"); emulator.set_fda({ url: __dirname + "/../../images/freedos722.img" }); emulator.keyboard_send_text("dir A:\n"); await emulator.wait_until_vga_screen_contains("FDOS "); + console.log("Got FDOS"); emulator.stop(); clearTimeout(timeout); + //clearInterval(interval); }, 1000); From 1afc297f8b61704716c6eb9bfb281d57036bd588 Mon Sep 17 00:00:00 2001 From: Christian Date: Mon, 7 Oct 2024 06:38:05 +0200 Subject: [PATCH 05/90] Add VGA graphical text mode (#1123) * Revert "Add VGA graphical text mode" This reverts commit f92e6b4b55c3f6e5a327b836375df20fedd45fa4. * started 2nd iteration of graphical text mode * fixed eslint complaints * include changes to main.js and cpu.js from f92e6b4, fix eslint error * added new methods in ScreenAdapter to DummyScreenAdapter * include changes to starter.js from f92e6b4 * changed text attribute BLINKING to FLAGS, introduced flags for BLINKING and FONT_PAGE_A * added support for cursor and blinking, made ScreenAdapter.FLAGS_* visible for VGAScreen * Variuos improvements and fixes. - improved implementation of the three display modes in ScreenAdapter - improved animation frame control (replaced "ScreenAdapter.stopped" with "cancelAnimationFrame()") - added method ScreenAdapter.grab_text_content() with documentation - fixed double-width (16px) font - improved detecting font plane 2 write access in VGAScreen (still work in progress) * fixed issues detected by eslint * fixed classical text mode * optimization: do not copy buffer to canvas when it's unmodified * invalidate all text rows when entering graphical text mode * Made sure that complete_redraw() is called whenever the state that put_char() depends on is modified. Method VGAScreen.put_char() depends on: 1. screen-wide BLINK_ENABLE bit 2. screen-wide PAGE_AB_ENABLE bit 3. VGAScreen.vga256_palette[] 4. VGAScreen.dac_mask 5. VGAScreen.dac_map For 1, 3 and 5 this was already the case. Added call to VGAScreen.complete_redraw() when PAGE_AB_ENABLE is changed. Added call to VGAScreen.complete_redraw() when VGAScreen.dac_map is changed. Added logic to mask out most significant foreground color bit if screen-wide PAGE_AB_ENABLE is true. * removed leftover comment * added new VGAScreen state variable font_page_ab_enabled to state[] * render internal font less often but more aggressively * fixed bug in ScreenAdapter.set_font_bitmap() * refactored ScreenAdapter - moved declarations of constants, member variables and methods into separate groups - moved scattered initialization code into init() method - removed redundant members and initialization code - refactored timer() method to simplify and clarify code - made control flow depend on three-state variable "mode" only * fixed bug in restoring state of graphical text: font_page was missing * restored accidentally lost cleanup code from commit f92e6b4 * narrowed scope of some variables * removed obsolete compatibility attributes from graphic_context * removed redundant initialization call to ScreenAdapter.set_mode() * allow overriding private member charmap[] to support external codepage mappings Introduce support for externally defined codepage mappings wherever charmap[] is used. - added member charmap_default[] with former content of charmap[] - made charmap[] initially point to charmap_default[] - added method set_charmap() to override or fall back to charmap_default[] * merged grab_text_content() into get_text_row()/get_text_screen() * removed comment as suggested * replaced boolean options.disable_autoscale with number options.scale options.scale defines the initial value for ScreenAdapter.scale_x and ScreenAdapter.scale_y. If undefined, options.scale defaults to 1. If options.scale is set to 0, 2x-upscale, scale_x, scale_y and HiDPI-adaption (window.devicePixelRatio) are all disabled in all three modes, they are in the responsibilty of host applications that define options.scale to be 0. * changed render_font_bitmap() to return void, reduced reallocation of font_bitmap[] * added guard to ensure that set_font_bitmap() is only called in text mode * changed text rendering method to glyph-blitting using canvas.drawImage() - replaced pixel-drawing with glyph-blitting - removed graphical_text_buffer - added 3 OffscreenCanvas objects for font bitmap, screen canvas and row helper - left state of canvas context ScreenAdapter.graphic_context untouched * improved cursor rendering * improved foreground color handling * minor changes * improved rendering of invisible glyphs An invisible glyph could be an ASCII Space (0x20) or Null (0x0) character, or any other empty glyph loaded into the VGA's font memory. Invisible glyphs (which are not rare) are currently drawn to the canvas exactly like regular glyphs, this patch handles that more efficiently. - added array with per-glyph visibility attribute - added invisible glyph detection in render_font_bitmap() - instead of drawing invisible glyphs in render_changed_rows(), now erase whole row buffer once per row and just skip invisible glyphs - removed one nested loop from render_font_bitmap() Characters hidden on screem caused by their blink attribute (rare) are also treated as invisible. * improved vga bit handling readabilty --- Makefile | 2 +- debug.html | 1 - src/browser/dummy_screen.js | 8 + src/browser/screen.js | 641 ++++++++++++++++++++++++++++-------- src/browser/starter.js | 4 +- src/vga.js | 193 +++++------ src/vga_text.js | 628 ----------------------------------- 7 files changed, 608 insertions(+), 869 deletions(-) delete mode 100644 src/vga_text.js diff --git a/Makefile b/Makefile index 3c135daa70..b6ee96ae5c 100644 --- a/Makefile +++ b/Makefile @@ -79,7 +79,7 @@ CARGO_FLAGS_SAFE=\ CARGO_FLAGS=$(CARGO_FLAGS_SAFE) -C target-feature=+bulk-memory -C target-feature=+multivalue -C target-feature=+simd128 CORE_FILES=const.js config.js io.js main.js lib.js buffer.js ide.js pci.js floppy.js \ - memory.js dma.js pit.js vga.js vga_text.js ps2.js rtc.js uart.js \ + memory.js dma.js pit.js vga.js ps2.js rtc.js uart.js \ acpi.js apic.js ioapic.js \ state.js ne2k.js sb16.js virtio.js virtio_console.js virtio_net.js \ bus.js log.js cpu.js debug.js \ diff --git a/debug.html b/debug.html index 57594cb869..a4e2d20c1a 100644 --- a/debug.html +++ b/debug.html @@ -20,7 +20,6 @@ - diff --git a/src/browser/dummy_screen.js b/src/browser/dummy_screen.js index 2f7757d0b0..405dd818f4 100644 --- a/src/browser/dummy_screen.js +++ b/src/browser/dummy_screen.js @@ -48,6 +48,14 @@ function DummyScreenAdapter() is_graphical = graphical; }; + this.set_font_bitmap = function(height, width_9px, width_dbl, copy_8th_col, bitmap, bitmap_changed) + { + }; + + this.set_font_page = function(page_a, page_b) + { + }; + this.clear_screen = function() { }; diff --git a/src/browser/screen.js b/src/browser/screen.js index 176ae429e3..d207d443b3 100644 --- a/src/browser/screen.js +++ b/src/browser/screen.js @@ -12,6 +12,22 @@ function ScreenAdapter(options, screen_fill_buffer) console.assert(screen_container, "options.container must be provided"); + const MODE_TEXT = 0; + const MODE_GRAPHICAL = 1; + const MODE_GRAPHICAL_TEXT = 2; + + const CHARACTER_INDEX = 0; + const FLAGS_INDEX = 1; + const BG_COLOR_INDEX = 2; + const FG_COLOR_INDEX = 3; + const TEXT_BUF_COMPONENT_SIZE = 4; + + const FLAG_BLINKING = 0x01; + const FLAG_FONT_PAGE_B = 0x02; + + this.FLAG_BLINKING = FLAG_BLINKING; + this.FLAG_FONT_PAGE_B = FLAG_FONT_PAGE_B; + var graphic_screen = screen_container.getElementsByTagName("canvas")[0], graphic_context = graphic_screen.getContext("2d", { alpha: false }), @@ -27,20 +43,20 @@ function ScreenAdapter(options, screen_fill_buffer) cursor_col, /** @type {number} */ - scale_x = 1, + scale_x = options.scale !== undefined ? options.scale : 1, /** @type {number} */ - scale_y = 1, + scale_y = options.scale !== undefined ? options.scale : 1, base_scale = 1, changed_rows, - // are we in graphical mode now? - is_graphical = !!options.use_graphical_text, + // current display mode: MODE_GRAPHICAL or either MODE_TEXT/MODE_GRAPHICAL_TEXT + mode, // Index 0: ASCII code - // Index 1: Blinking + // Index 1: Flags bitset (see FLAG_...) // Index 2: Background color // Index 3: Foreground color text_mode_data, @@ -49,16 +65,40 @@ function ScreenAdapter(options, screen_fill_buffer) text_mode_width, // number of rows - text_mode_height; - - const CHARACTER_INDEX = 0; - const BLINKING_INDEX = 1; - const BG_COLOR_INDEX = 2; - const FG_COLOR_INDEX = 3; - const TEXT_MODE_COMPONENT_SIZE = 4; - - var stopped = false; - var paused = false; + text_mode_height, + + // graphical text mode's offscreen canvas contexts + offscreen_context, + offscreen_extra_context, + + // fonts + font_context, + font_image_data, + font_is_visible = new Int8Array(8 * 256), + font_height, + font_width, + font_width_9px, + font_width_dbl, + font_copy_8th_col, + font_page_a = 0, + font_page_b = 0, + + // blink state + blink_visible, + tm_last_update = 0, + + // cursor attributes + cursor_start, + cursor_end, + cursor_enabled, + + // 8-bit Unicode character maps + charmap_default = [], + charmap = charmap_default, + + // render loop state + timer_id = 0, + paused = false; // 0x12345 -> "#012345" function number_as_color(n) @@ -67,86 +107,276 @@ function ScreenAdapter(options, screen_fill_buffer) return "#" + "0".repeat(6 - n.length) + n; } - - /** - * Charmaps that constraint unicode sequences for the default dospage - * @const - */ - var charmap_high = new Uint16Array([ - 0x2302, - 0xC7, 0xFC, 0xE9, 0xE2, 0xE4, 0xE0, 0xE5, 0xE7, - 0xEA, 0xEB, 0xE8, 0xEF, 0xEE, 0xEC, 0xC4, 0xC5, - 0xC9, 0xE6, 0xC6, 0xF4, 0xF6, 0xF2, 0xFB, 0xF9, - 0xFF, 0xD6, 0xDC, 0xA2, 0xA3, 0xA5, 0x20A7, 0x192, - 0xE1, 0xED, 0xF3, 0xFA, 0xF1, 0xD1, 0xAA, 0xBA, - 0xBF, 0x2310, 0xAC, 0xBD, 0xBC, 0xA1, 0xAB, 0xBB, - 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, - 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, - 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F, - 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, - 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, - 0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, - 0x3B1, 0xDF, 0x393, 0x3C0, 0x3A3, 0x3C3, 0xB5, 0x3C4, - 0x3A6, 0x398, 0x3A9, 0x3B4, 0x221E, 0x3C6, 0x3B5, 0x2229, - 0x2261, 0xB1, 0x2265, 0x2264, 0x2320, 0x2321, 0xF7, - 0x2248, 0xB0, 0x2219, 0xB7, 0x221A, 0x207F, 0xB2, 0x25A0, 0xA0 - ]); - - /** @const */ - var charmap_low = new Uint16Array([ - 0x20, 0x263A, 0x263B, 0x2665, 0x2666, 0x2663, 0x2660, 0x2022, - 0x25D8, 0x25CB, 0x25D9, 0x2642, 0x2640, 0x266A, 0x266B, 0x263C, - 0x25BA, 0x25C4, 0x2195, 0x203C, 0xB6, 0xA7, 0x25AC, 0x21A8, - 0x2191, 0x2193, 0x2192, 0x2190, 0x221F, 0x2194, 0x25B2, 0x25BC - ]); - - var charmap = [], - chr; - - for(var i = 0; i < 256; i++) - { - if(i > 126) - { - chr = charmap_high[i - 0x7F]; - } - else if(i < 32) - { - chr = charmap_low[i]; + function render_font_bitmap(vga_bitmap) + { + // - Browsers impose limts on the X- and Y-axes of bitmaps (typically around 8 to 32k). + // Draw the 8 VGA font pages of 256 glyphs in 8 rows of 256 columns, this results + // in 2048, 2304 or 4096px on the X-axis (for 8, 9 or 16px VGA font width, resp.). + // This 2d layout is also convenient for glyph lookup when rendering text. + // - Font bitmap pixels are black and either fully opaque (alpha 255) or fully transparent (0). + const bitmap_width = font_width * 256; + const bitmap_height = font_height * 8; + + let font_canvas = font_context ? font_context.canvas : null; + if(!font_canvas || font_canvas.width !== bitmap_width || font_canvas.height !== bitmap_height) + { + if(!font_canvas) + { + font_canvas = new OffscreenCanvas(bitmap_width, bitmap_height); + font_context = font_canvas.getContext("2d"); + } + else + { + font_canvas.width = bitmap_width; + font_canvas.height = bitmap_height; + } + font_image_data = font_context.createImageData(bitmap_width, bitmap_height); } - else - { - chr = i; + + const font_bitmap = font_image_data.data; + let i_dst = 0, is_visible; + const put_bit = font_width_dbl ? + function(value) + { + is_visible = is_visible || value; + font_bitmap[i_dst + 3] = value; + font_bitmap[i_dst + 7] = value; + i_dst += 8; + } : + function(value) + { + is_visible = is_visible || value; + font_bitmap[i_dst + 3] = value; + i_dst += 4; + }; + + // move i_vga from end of glyph to start of next glyph + const vga_inc_chr = 32 - font_height; + // move i_dst from end of font page (bitmap row) to start of next font page + const dst_inc_row = bitmap_width * (font_height - 1) * 4; + // move i_dst from end of glyph (bitmap column) to start of next glyph + const dst_inc_col = (font_width - bitmap_width * font_height) * 4; + // move i_dst from end of a glyph's scanline to start of its next scanline + const dst_inc_line = font_width * 255 * 4; + + for(let i_chr_all = 0, i_vga = 0; i_chr_all < 2048; ++i_chr_all, i_vga += vga_inc_chr, i_dst += dst_inc_col) + { + const i_chr = i_chr_all % 256; + if(i_chr_all && !i_chr) + { + i_dst += dst_inc_row; + } + is_visible = false; + for(let i_line = 0; i_line < font_height; ++i_line, ++i_vga, i_dst += dst_inc_line) + { + const line_bits = vga_bitmap[i_vga]; + for(let i_bit = 0x80; i_bit > 0; i_bit >>= 1) + { + put_bit(line_bits & i_bit ? 255 : 0); + } + if(font_width_9px) + { + put_bit(font_copy_8th_col && i_chr >= 0xC0 && i_chr <= 0xDF && line_bits & 1 ? 255 : 0); + } + } + font_is_visible[i_chr_all] = is_visible ? 1 : 0; } - charmap[i] = String.fromCharCode(chr); + font_context.putImageData(font_image_data, 0, 0); } - graphic_context.imageSmoothingEnabled = false; + function render_changed_rows() + { + const font_canvas = font_context.canvas; + const offscreen_extra_canvas = offscreen_extra_context.canvas; + const txt_row_size = text_mode_width * TEXT_BUF_COMPONENT_SIZE; + const gfx_width = text_mode_width * font_width; + const row_extra_1_y = 0; + const row_extra_2_y = font_height; + + let n_rows_rendered = 0; + for(let row_i = 0, row_y = 0, txt_i = 0; row_i < text_mode_height; ++row_i, row_y += font_height) + { + if(!changed_rows[row_i]) + { + txt_i += txt_row_size; + continue; + } + ++n_rows_rendered; - cursor_element.classList.add("cursor"); - cursor_element.style.position = "absolute"; - cursor_element.style.backgroundColor = "#ccc"; - cursor_element.style.width = "7px"; - cursor_element.style.display = "inline-block"; + // clear extra row 2 + offscreen_extra_context.clearRect(0, row_extra_2_y, gfx_width, font_height); - text_screen.style.display = "block"; - graphic_screen.style.display = "none"; + let fg_rgba, fg_x, bg_rgba, bg_x; + for(let col_x = 0; col_x < gfx_width; col_x += font_width, txt_i += TEXT_BUF_COMPONENT_SIZE) + { + const chr = text_mode_data[txt_i + CHARACTER_INDEX]; + const chr_flags = text_mode_data[txt_i + FLAGS_INDEX]; + const chr_bg_rgba = text_mode_data[txt_i + BG_COLOR_INDEX]; + const chr_fg_rgba = text_mode_data[txt_i + FG_COLOR_INDEX]; + const chr_font_page = chr_flags & FLAG_FONT_PAGE_B ? font_page_b : font_page_a; + const chr_visible = (!(chr_flags & FLAG_BLINKING) || blink_visible) && font_is_visible[(chr_font_page << 8) + chr]; + + if(bg_rgba !== chr_bg_rgba) + { + if(bg_rgba !== undefined) + { + // draw opaque block of background color into offscreen_context + offscreen_context.fillStyle = number_as_color(bg_rgba); + offscreen_context.fillRect(bg_x, row_y, col_x - bg_x, font_height); + } + bg_rgba = chr_bg_rgba; + bg_x = col_x; + } + + if(fg_rgba !== chr_fg_rgba) + { + if(fg_rgba !== undefined) + { + // draw opaque block of foreground color into extra row 1 + offscreen_extra_context.fillStyle = number_as_color(fg_rgba); + offscreen_extra_context.fillRect(fg_x, row_extra_1_y, col_x - fg_x, font_height); + } + fg_rgba = chr_fg_rgba; + fg_x = col_x; + } + + if(chr_visible) + { + // copy transparent glyphs into extra row 2 + offscreen_extra_context.drawImage(font_canvas, + chr * font_width, chr_font_page * font_height, font_width, font_height, + col_x, row_extra_2_y, font_width, font_height); + } + } + + // draw rightmost block of foreground color into extra row 1 + offscreen_extra_context.fillStyle = number_as_color(fg_rgba); + offscreen_extra_context.fillRect(fg_x, row_extra_1_y, gfx_width - fg_x, font_height); + + // combine extra row 1 (colors) and 2 (glyphs) into extra row 1 (colored glyphs) + offscreen_extra_context.globalCompositeOperation = "destination-in"; + offscreen_extra_context.drawImage(offscreen_extra_canvas, + 0, row_extra_2_y, gfx_width, font_height, + 0, row_extra_1_y, gfx_width, font_height); + offscreen_extra_context.globalCompositeOperation = "source-over"; + + // draw rightmost block of background color into offscreen_context + offscreen_context.fillStyle = number_as_color(bg_rgba); + offscreen_context.fillRect(bg_x, row_y, gfx_width - bg_x, font_height); + + // copy colored glyphs from extra row 1 into offscreen_context (on top of background colors) + offscreen_context.drawImage(offscreen_extra_canvas, + 0, row_extra_1_y, gfx_width, font_height, + 0, row_y, gfx_width, font_height); + } + + if(n_rows_rendered) + { + if(blink_visible && cursor_enabled && changed_rows[cursor_row]) + { + const cursor_txt_i = (cursor_row * text_mode_width + cursor_col) * TEXT_BUF_COMPONENT_SIZE; + const cursor_rgba = text_mode_data[cursor_txt_i + FG_COLOR_INDEX]; + offscreen_context.fillStyle = number_as_color(cursor_rgba); + offscreen_context.fillRect( + cursor_col * font_width, + cursor_row * font_height + cursor_start, + font_width, + cursor_end - cursor_start + 1); + } + changed_rows.fill(0); + } + + return n_rows_rendered; + } + + function mark_blinking_rows_dirty() + { + const txt_row_size = text_mode_width * TEXT_BUF_COMPONENT_SIZE; + for(let row_i = 0, txt_i = 0; row_i < text_mode_height; ++row_i) + { + if(changed_rows[row_i]) + { + txt_i += txt_row_size; + continue; + } + for(let col_i = 0; col_i < text_mode_width; ++col_i, txt_i += TEXT_BUF_COMPONENT_SIZE) + { + if(text_mode_data[txt_i + FLAGS_INDEX] & FLAG_BLINKING) + { + changed_rows[row_i] = 1; + txt_i += txt_row_size - col_i * TEXT_BUF_COMPONENT_SIZE; + break; + } + } + } + } this.init = function() { - // initialize with mode and size presets as expected by the bios - // to avoid flickering during early startup - this.set_mode(is_graphical); + // map 8-bit DOS codepage 437 character range 0-31 to 16-bit Unicode codepoints + const charmap_low = new Uint16Array([ + 0x20, 0x263A, 0x263B, 0x2665, 0x2666, 0x2663, 0x2660, 0x2022, + 0x25D8, 0x25CB, 0x25D9, 0x2642, 0x2640, 0x266A, 0x266B, 0x263C, + 0x25BA, 0x25C4, 0x2195, 0x203C, 0xB6, 0xA7, 0x25AC, 0x21A8, + 0x2191, 0x2193, 0x2192, 0x2190, 0x221F, 0x2194, 0x25B2, 0x25BC + ]); + // map 8-bit DOS codepage 437 character range 127-255 to 16-bit Unicode codepoints + const charmap_high = new Uint16Array([ + 0x2302, + 0xC7, 0xFC, 0xE9, 0xE2, 0xE4, 0xE0, 0xE5, 0xE7, + 0xEA, 0xEB, 0xE8, 0xEF, 0xEE, 0xEC, 0xC4, 0xC5, + 0xC9, 0xE6, 0xC6, 0xF4, 0xF6, 0xF2, 0xFB, 0xF9, + 0xFF, 0xD6, 0xDC, 0xA2, 0xA3, 0xA5, 0x20A7, 0x192, + 0xE1, 0xED, 0xF3, 0xFA, 0xF1, 0xD1, 0xAA, 0xBA, + 0xBF, 0x2310, 0xAC, 0xBD, 0xBC, 0xA1, 0xAB, 0xBB, + 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, + 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, + 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F, + 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, + 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, + 0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, + 0x3B1, 0xDF, 0x393, 0x3C0, 0x3A3, 0x3C3, 0xB5, 0x3C4, + 0x3A6, 0x398, 0x3A9, 0x3B4, 0x221E, 0x3C6, 0x3B5, 0x2229, + 0x2261, 0xB1, 0x2265, 0x2264, 0x2320, 0x2321, 0xF7, + 0x2248, 0xB0, 0x2219, 0xB7, 0x221A, 0x207F, 0xB2, 0x25A0, 0xA0 + ]); + + // initialize 8-bit DOS codepage 437 map charmap[256] (Uint8 -> String[1]) + for(var i = 0, chr; i < 256; i++) + { + if(i > 126) + { + chr = charmap_high[i - 0x7F]; + } + else if(i < 32) + { + chr = charmap_low[i]; + } + else + { + chr = i; + } + charmap_default.push(String.fromCharCode(chr)); + } - if(is_graphical) + // setup text mode cursor DOM element + cursor_element.classList.add("cursor"); + cursor_element.style.position = "absolute"; + cursor_element.style.backgroundColor = "#ccc"; + cursor_element.style.width = "7px"; + cursor_element.style.display = "inline-block"; + + // initialize display mode and size to 80x25 text with 9x16 font + this.set_mode(false); + this.set_size_text(80, 25); + if(mode === MODE_GRAPHICAL_TEXT) { - // assume 80x25 with 9x16 font this.set_size_graphical(720, 400, 720, 400); } - else - { - this.set_size_text(80, 25); - } + + // initialize CSS scaling + this.set_scale(scale_x, scale_y); this.timer(); }; @@ -155,7 +385,7 @@ function ScreenAdapter(options, screen_fill_buffer) { const image = new Image(); - if(is_graphical) + if(mode === MODE_GRAPHICAL || mode === MODE_GRAPHICAL_TEXT) { image.src = graphic_screen.toDataURL("image/png"); } @@ -176,7 +406,7 @@ function ScreenAdapter(options, screen_fill_buffer) { for(let x = 0; x < text_mode_width; x++) { - const index = (y * text_mode_width + x) * TEXT_MODE_COMPONENT_SIZE; + const index = (y * text_mode_width + x) * TEXT_BUF_COMPONENT_SIZE; const character = text_mode_data[index + CHARACTER_INDEX]; const bg_color = text_mode_data[index + BG_COLOR_INDEX]; const fg_color = text_mode_data[index + FG_COLOR_INDEX]; @@ -204,16 +434,16 @@ function ScreenAdapter(options, screen_fill_buffer) return image; }; - this.put_char = function(row, col, chr, blinking, bg_color, fg_color) + this.put_char = function(row, col, chr, flags, bg_color, fg_color) { dbg_assert(row >= 0 && row < text_mode_height); dbg_assert(col >= 0 && col < text_mode_width); dbg_assert(chr >= 0 && chr < 0x100); - const p = TEXT_MODE_COMPONENT_SIZE * (row * text_mode_width + col); + const p = TEXT_BUF_COMPONENT_SIZE * (row * text_mode_width + col); text_mode_data[p + CHARACTER_INDEX] = chr; - text_mode_data[p + BLINKING_INDEX] = blinking; + text_mode_data[p + FLAGS_INDEX] = flags; text_mode_data[p + BG_COLOR_INDEX] = bg_color; text_mode_data[p + FG_COLOR_INDEX] = fg_color; @@ -222,10 +452,27 @@ function ScreenAdapter(options, screen_fill_buffer) this.timer = function() { - if(!stopped) + timer_id = requestAnimationFrame(() => this.update_screen()); + }; + + this.update_screen = function() + { + if(!paused) { - requestAnimationFrame(() => is_graphical ? this.update_graphical() : this.update_text()); + if(mode === MODE_TEXT) + { + this.update_text(); + } + else if(mode === MODE_GRAPHICAL) + { + this.update_graphical(); + } + else + { + this.update_graphical_text(); + } } + this.timer(); }; this.update_text = function() @@ -238,22 +485,44 @@ function ScreenAdapter(options, screen_fill_buffer) changed_rows[i] = 0; } } - - this.timer(); }; this.update_graphical = function() { - if(!paused) + this.screen_fill_buffer(); + }; + + this.update_graphical_text = function() + { + if(offscreen_context) { - this.screen_fill_buffer(); + // toggle cursor and blinking character visibility at a frequency of ~3.75hz + const tm_now = performance.now(); + if(tm_now - tm_last_update > 266) + { + blink_visible = !blink_visible; + if(cursor_enabled) + { + changed_rows[cursor_row] = 1; + } + mark_blinking_rows_dirty(); + tm_last_update = tm_now; + } + // copy to DOM canvas only if anything new was rendered + if(render_changed_rows()) + { + graphic_context.drawImage(offscreen_context.canvas, 0, 0); + } } - this.timer(); }; this.destroy = function() { - stopped = true; + if(timer_id) + { + cancelAnimationFrame(timer_id); + timer_id = 0; + } }; this.pause = function() @@ -261,6 +530,7 @@ function ScreenAdapter(options, screen_fill_buffer) paused = true; cursor_element.classList.remove("blinking-cursor"); }; + this.continue = function() { paused = false; @@ -269,17 +539,57 @@ function ScreenAdapter(options, screen_fill_buffer) this.set_mode = function(graphical) { - is_graphical = graphical; + mode = graphical ? MODE_GRAPHICAL : (options.use_graphical_text ? MODE_GRAPHICAL_TEXT : MODE_TEXT); - if(graphical) + if(mode === MODE_TEXT) + { + text_screen.style.display = "block"; + graphic_screen.style.display = "none"; + } + else { text_screen.style.display = "none"; graphic_screen.style.display = "block"; + + if(mode === MODE_GRAPHICAL_TEXT && changed_rows) + { + changed_rows.fill(1); + } } - else + }; + + this.set_font_bitmap = function(height, width_9px, width_dbl, copy_8th_col, vga_bitmap, vga_bitmap_changed) + { + const width = width_dbl ? 16 : (width_9px ? 9 : 8); + if(font_height !== height || font_width !== width || font_width_9px !== width_9px || + font_width_dbl !== width_dbl || font_copy_8th_col !== copy_8th_col || + vga_bitmap_changed) + { + const size_changed = font_width !== width || font_height !== height; + font_height = height; + font_width = width; + font_width_9px = width_9px; + font_width_dbl = width_dbl; + font_copy_8th_col = copy_8th_col; + if(mode === MODE_GRAPHICAL_TEXT) + { + render_font_bitmap(vga_bitmap); + changed_rows.fill(1); + if(size_changed) + { + this.set_size_graphical_text(); + } + } + } + }; + + this.set_font_page = function(page_a, page_b) + { + if(font_page_a !== page_a || font_page_b !== page_b) { - text_screen.style.display = "block"; - graphic_screen.style.display = "none"; + font_page_a = page_a; + font_page_b = page_b; + changed_rows.fill(1); } }; @@ -289,6 +599,44 @@ function ScreenAdapter(options, screen_fill_buffer) graphic_context.fillRect(0, 0, graphic_screen.width, graphic_screen.height); }; + this.set_size_graphical_text = function() + { + if(!font_context) + { + return; + } + + const gfx_width = font_width * text_mode_width; + const gfx_height = font_height * text_mode_height; + const offscreen_extra_height = font_height * 2; + + if(!offscreen_context || offscreen_context.canvas.width !== gfx_width || + offscreen_context.canvas.height !== gfx_height || + offscreen_extra_context.canvas.height !== offscreen_extra_height) + { + // resize offscreen canvases + if(!offscreen_context) + { + const offscreen_canvas = new OffscreenCanvas(gfx_width, gfx_height); + offscreen_context = offscreen_canvas.getContext("2d", { alpha: false }); + const offscreen_extra_canvas = new OffscreenCanvas(gfx_width, offscreen_extra_height); + offscreen_extra_context = offscreen_extra_canvas.getContext("2d"); + } + else + { + offscreen_context.canvas.width = gfx_width; + offscreen_context.canvas.height = gfx_height; + offscreen_extra_context.canvas.width = gfx_width; + offscreen_extra_context.canvas.height = offscreen_extra_height; + } + + // resize DOM canvas graphic_screen + this.set_size_graphical(gfx_width, gfx_height, gfx_width, gfx_height); + + changed_rows.fill(1); + } + }; + /** * @param {number} cols * @param {number} rows @@ -301,27 +649,34 @@ function ScreenAdapter(options, screen_fill_buffer) } changed_rows = new Int8Array(rows); - text_mode_data = new Int32Array(cols * rows * TEXT_MODE_COMPONENT_SIZE); + text_mode_data = new Int32Array(cols * rows * TEXT_BUF_COMPONENT_SIZE); text_mode_width = cols; text_mode_height = rows; - while(text_screen.childNodes.length > rows) + if(mode === MODE_TEXT) { - text_screen.removeChild(text_screen.firstChild); - } + while(text_screen.childNodes.length > rows) + { + text_screen.removeChild(text_screen.firstChild); + } - while(text_screen.childNodes.length < rows) - { - text_screen.appendChild(document.createElement("div")); - } + while(text_screen.childNodes.length < rows) + { + text_screen.appendChild(document.createElement("div")); + } + + for(var i = 0; i < rows; i++) + { + this.text_update_row(i); + } - for(var i = 0; i < rows; i++) + update_scale_text(); + } + else if(mode === MODE_GRAPHICAL_TEXT) { - this.text_update_row(i); + this.set_size_graphical_text(); } - - update_scale_text(); }; this.set_size_graphical = function(width, height, buffer_width, buffer_height) @@ -339,12 +694,12 @@ function ScreenAdapter(options, screen_fill_buffer) graphic_screen.width = width; graphic_screen.height = height; - // graphic_context loses its configuration when its graphic_screen gets resized, reinitialize + + // graphic_context must be reconfigured whenever its graphic_screen is resized graphic_context.imageSmoothingEnabled = false; // add some scaling to tiny resolutions - if(!options.disable_autoscale && - width <= 640 && + if(width <= 640 && width * 2 < window.innerWidth * window.devicePixelRatio && height * 2 < window.innerHeight * window.devicePixelRatio) { @@ -358,6 +713,11 @@ function ScreenAdapter(options, screen_fill_buffer) update_scale_graphic(); }; + this.set_charmap = function(text_charmap) + { + charmap = text_charmap || charmap_default; + }; + this.set_scale = function(s_x, s_y) { scale_x = s_x; @@ -366,7 +726,6 @@ function ScreenAdapter(options, screen_fill_buffer) update_scale_text(); update_scale_graphic(); }; - this.set_scale(scale_x, scale_y); function update_scale_text() { @@ -380,6 +739,11 @@ function ScreenAdapter(options, screen_fill_buffer) function elem_set_scale(elem, scale_x, scale_y, use_scale) { + if(!scale_x || !scale_y) + { + return; + } + elem.style.width = ""; elem.style.height = ""; @@ -433,17 +797,34 @@ function ScreenAdapter(options, screen_fill_buffer) } } - this.update_cursor_scanline = function(start, end, visible) + this.update_cursor_scanline = function(start, end, enabled) { - if(visible) - { - cursor_element.style.display = "inline"; - cursor_element.style.height = (end - start) + "px"; - cursor_element.style.marginTop = start + "px"; - } - else + if(start !== cursor_start || end !== cursor_end || enabled !== cursor_enabled) { - cursor_element.style.display = "none"; + if(mode === MODE_TEXT) + { + if(enabled) + { + cursor_element.style.display = "inline"; + cursor_element.style.height = (end - start) + "px"; + cursor_element.style.marginTop = start + "px"; + } + else + { + cursor_element.style.display = "none"; + } + } + else if(mode === MODE_GRAPHICAL_TEXT) + { + if(cursor_row < text_mode_height) + { + changed_rows[cursor_row] = 1; + } + } + + cursor_start = start; + cursor_end = end; + cursor_enabled = enabled; } }; @@ -467,7 +848,7 @@ function ScreenAdapter(options, screen_fill_buffer) this.text_update_row = function(row) { - var offset = TEXT_MODE_COMPONENT_SIZE * row * text_mode_width, + var offset = TEXT_BUF_COMPONENT_SIZE * row * text_mode_width, row_element, color_element, fragment; @@ -484,7 +865,7 @@ function ScreenAdapter(options, screen_fill_buffer) { color_element = document.createElement("span"); - blinking = text_mode_data[offset + BLINKING_INDEX]; + blinking = text_mode_data[offset + FLAGS_INDEX] & FLAG_BLINKING; bg_color = text_mode_data[offset + BG_COLOR_INDEX]; fg_color = text_mode_data[offset + FG_COLOR_INDEX]; @@ -500,7 +881,7 @@ function ScreenAdapter(options, screen_fill_buffer) // put characters of the same color in one element while(i < text_mode_width && - text_mode_data[offset + BLINKING_INDEX] === blinking && + (text_mode_data[offset + FLAGS_INDEX] & FLAG_BLINKING) === blinking && text_mode_data[offset + BG_COLOR_INDEX] === bg_color && text_mode_data[offset + FG_COLOR_INDEX] === fg_color) { @@ -510,7 +891,7 @@ function ScreenAdapter(options, screen_fill_buffer) dbg_assert(charmap[ascii]); i++; - offset += TEXT_MODE_COMPONENT_SIZE; + offset += TEXT_BUF_COMPONENT_SIZE; if(row === cursor_row) { @@ -591,9 +972,9 @@ function ScreenAdapter(options, screen_fill_buffer) for(let x = 0; x < text_mode_width; x++) { - const index = (y * text_mode_width + x) * TEXT_MODE_COMPONENT_SIZE; + const index = (y * text_mode_width + x) * TEXT_BUF_COMPONENT_SIZE; const character = text_mode_data[index + CHARACTER_INDEX]; - result += String.fromCharCode(character); + result += charmap[character]; } return result; diff --git a/src/browser/starter.js b/src/browser/starter.js index 114e1f0898..528a81bb63 100644 --- a/src/browser/starter.js +++ b/src/browser/starter.js @@ -58,7 +58,7 @@ * * - `screen Object` (No screen) - An object with the following properties: * - `container HTMLElement` - An HTMLElement, see above. - * - `disable_autoscale boolean` (false) - Disable automatic scaling of small resolutions. + * - `scale` (1) - Set initial scale_x and scale_y, if 0 disable automatic upscaling and dpi-adaption * * *** * @@ -101,7 +101,7 @@ disable_keyboard: (boolean|undefined), wasm_fn: (Function|undefined), screen: ({ - disable_autoscale: (boolean|undefined), + scale: (number|undefined), } | undefined), }} options * @constructor diff --git a/src/vga.js b/src/vga.js index 2f34ccb738..660f61c3c8 100644 --- a/src/vga.js +++ b/src/vga.js @@ -294,6 +294,7 @@ function VGAScreen(cpu, bus, screen, vga_memory_size, options) this.miscellaneous_output_register = 0xff; this.port_3DA_value = 0xFF; + this.font_page_ab_enabled = false; var io = cpu.io; @@ -377,41 +378,9 @@ function VGAScreen(cpu, bus, screen, vga_memory_size, options) (addr, value) => this.vga_memory_write(addr, value), ); - if(options.use_graphical_text) - { - this.graphical_text = new GraphicalText(this); - } - cpu.devices.pci.register_device(this); } -VGAScreen.prototype.grab_text_content = function(keep_whitespace) -{ - var addr = this.start_address << 1; - const split_screen_row = this.scan_line_to_screen_row(this.line_compare); - const row_offset = Math.max(0, (this.offset_register * 2 - this.max_cols) * 2); - const text_rows = []; - - for(var row = 0; row < this.max_rows; row++) - { - if(row === split_screen_row) - { - addr = 0; - } - - let line = ""; - for(var col = 0; col < this.max_cols; col++, addr += 2) - { - line += String.fromCodePoint(this.vga_memory[addr]); - } - - text_rows.push(keep_whitespace ? line : line.trimEnd()); - addr += row_offset; - } - - return text_rows; -}; - VGAScreen.prototype.get_state = function() { var state = []; @@ -480,6 +449,7 @@ VGAScreen.prototype.get_state = function() state[61] = this.pixel_buffer; state[62] = this.dac_mask; state[63] = this.character_map_select; + state[64] = this.font_page_ab_enabled; return state; }; @@ -550,8 +520,9 @@ VGAScreen.prototype.set_state = function(state) state[61] && this.pixel_buffer.set(state[61]); this.dac_mask = state[62] === undefined ? 0xFF : state[62]; this.character_map_select = state[63] === undefined ? 0 : state[63]; + this.font_page_ab_enabled = state[64] === undefined ? 0 : state[64]; - this.screen.set_mode(this.graphical_mode || !!this.graphical_text); + this.screen.set_mode(this.graphical_mode); if(this.graphical_mode) { @@ -573,7 +544,9 @@ VGAScreen.prototype.set_state = function(state) } else { + this.set_font_bitmap(true); this.set_size_text(this.max_cols, this.max_rows); + this.set_font_page(); this.update_cursor_scanline(); this.update_cursor(); } @@ -676,13 +649,16 @@ VGAScreen.prototype.vga_memory_write = function(addr, value) { this.vga_memory_write_graphical(addr, value); } - else + else if(!(this.plane_write_bm & 0x3)) { - if(!(this.plane_write_bm & 0x3)) + if(this.plane_write_bm & 0x4) { + // write to plane 2 (font-bitmap) this.plane2[addr] = value; - return; } + } + else + { this.vga_memory_write_text_mode(addr, value); } }; @@ -855,15 +831,13 @@ VGAScreen.prototype.apply_bitmask = function(data_dword, bitmask_dword) VGAScreen.prototype.text_mode_redraw = function() { - if(this.graphical_text) - { - return; - } - const split_screen_row = this.scan_line_to_screen_row(this.line_compare); const row_offset = Math.max(0, (this.offset_register * 2 - this.max_cols) * 2); - const blink_flag = this.attribute_mode & 1 << 3; - const bg_color_mask = blink_flag ? 7 : 0xF; + const blink_enabled = this.attribute_mode & 1 << 3; + const fg_color_mask = this.font_page_ab_enabled ? 7 : 0xF; + const bg_color_mask = blink_enabled ? 7 : 0xF; + const FLAG_BLINKING = this.screen.FLAG_BLINKING; + const FLAG_FONT_PAGE_B = this.screen.FLAG_FONT_PAGE_B; let addr = this.start_address << 1; @@ -878,13 +852,15 @@ VGAScreen.prototype.text_mode_redraw = function() { const chr = this.vga_memory[addr]; const color = this.vga_memory[addr | 1]; - const blinking = blink_flag && (color & 1 << 7); + const blinking = blink_enabled && (color & 1 << 7); + const font_page_b = this.font_page_ab_enabled && !(color & 1 << 3); + const flags = (blinking ? FLAG_BLINKING : 0) | (font_page_b ? FLAG_FONT_PAGE_B : 0); this.bus.send("screen-put-char", [row, col, chr]); - this.screen.put_char(row, col, chr, blinking, + this.screen.put_char(row, col, chr, flags, this.vga256_palette[this.dac_mask & this.dac_map[color >> 4 & bg_color_mask]], - this.vga256_palette[this.dac_mask & this.dac_map[color & 0xF]]); + this.vga256_palette[this.dac_mask & this.dac_map[color & fg_color_mask]]); addr += 2; } @@ -935,23 +911,18 @@ VGAScreen.prototype.vga_memory_write_text_mode = function(addr, value) chr = value; color = this.vga_memory[addr | 1]; } - - const blink_flag = this.attribute_mode & 1 << 3; - const blinking = blink_flag && (color & 1 << 7); - const bg_color_mask = blink_flag ? 7 : 0xF; + const blink_enabled = this.attribute_mode & 1 << 3; + const blinking = blink_enabled && (color & 1 << 7); + const font_page_b = this.font_page_ab_enabled && !(color & 1 << 3); + const flags = (blinking ? this.screen.FLAG_BLINKING : 0) | (font_page_b ? this.screen.FLAG_FONT_PAGE_B : 0); + const fg_color_mask = this.font_page_ab_enabled ? 7 : 0xF; + const bg_color_mask = blink_enabled ? 7 : 0xF; this.bus.send("screen-put-char", [row, col, chr]); - if(this.graphical_text) - { - this.graphical_text.invalidate_row(row); - } - else - { - this.screen.put_char(row, col, chr, blinking, - this.vga256_palette[this.dac_mask & this.dac_map[color >> 4 & bg_color_mask]], - this.vga256_palette[this.dac_mask & this.dac_map[color & 0xF]]); - } + this.screen.put_char(row, col, chr, flags, + this.vga256_palette[this.dac_mask & this.dac_map[color >> 4 & bg_color_mask]], + this.vga256_palette[this.dac_mask & this.dac_map[color & fg_color_mask]]); }; VGAScreen.prototype.update_cursor = function() @@ -972,16 +943,9 @@ VGAScreen.prototype.update_cursor = function() } dbg_assert(row >= 0 && col >= 0); - // NOTE: is allowed to be out of bounds - if(this.graphical_text) - { - this.graphical_text.set_cursor_pos(row, col); - } - else - { - this.screen.update_cursor(row, col); - } + // NOTE: is allowed to be out of bounds + this.screen.update_cursor(row, col); }; VGAScreen.prototype.complete_redraw = function() @@ -1174,16 +1138,8 @@ VGAScreen.prototype.set_size_text = function(cols_count, rows_count) this.max_cols = cols_count; this.max_rows = rows_count; + this.screen.set_size_text(cols_count, rows_count); this.bus.send("screen-set-size", [cols_count, rows_count, 0]); - - if(this.graphical_text) - { - this.graphical_text.set_size(rows_count, cols_count); - } - else - { - this.screen.set_size_text(cols_count, rows_count); - } }; VGAScreen.prototype.set_size_graphical = function(width, height, virtual_width, virtual_height, bpp) @@ -1401,15 +1357,7 @@ VGAScreen.prototype.update_cursor_scanline = function() const start = Math.min(max, this.cursor_scanline_start & 0x1F); const end = Math.min(max, this.cursor_scanline_end & 0x1F); const visible = !disabled && start < end; - - if(this.graphical_text) - { - this.graphical_text.set_cursor_attr(start, end, visible); - } - else - { - this.screen.update_cursor_scanline(start, end, visible); - } + this.screen.update_cursor_scanline(start, end, visible); }; /** @@ -1460,7 +1408,7 @@ VGAScreen.prototype.port3C0_write = function(value) if(!this.svga_enabled && this.graphical_mode !== is_graphical) { this.graphical_mode = is_graphical; - this.screen.set_mode(this.graphical_mode || !!this.graphical_text); + this.screen.set_mode(this.graphical_mode); } if((previous_mode ^ value) & 0x40) @@ -1473,6 +1421,8 @@ VGAScreen.prototype.port3C0_write = function(value) // Data stored in image buffer are invalidated this.complete_redraw(); + + this.set_font_bitmap(false); } break; case 0x12: @@ -1588,24 +1538,25 @@ VGAScreen.prototype.port3C5_write = function(value) // Screen disable bit modified this.update_layers(); } + this.set_font_bitmap(false); break; case 0x02: dbg_log("plane write mask: " + h(value), LOG_VGA); var previous_plane_write_bm = this.plane_write_bm; this.plane_write_bm = value; - if(this.graphical_text && previous_plane_write_bm !== 0xf && (previous_plane_write_bm & 0x4) && !(this.plane_write_bm & 0x4)) + if(!this.graphical_mode && previous_plane_write_bm & 0x4 && !(this.plane_write_bm & 0x4)) { - // End of font plane 2 write access (initial value of plane_write_bm assumed to be 0xf) - this.graphical_text.invalidate_font_shape(); + // End of font plane 2 write access + this.set_font_bitmap(true); } break; case 0x03: dbg_log("character map select: " + h(value), LOG_VGA); var previous_character_map_select = this.character_map_select; this.character_map_select = value; - if(this.graphical_text && previous_character_map_select !== this.character_map_select) + if(!this.graphical_mode && previous_character_map_select !== value) { - this.graphical_text.set_character_map(this.character_map_select); + this.set_font_page(); } break; case 0x04: @@ -1640,7 +1591,11 @@ VGAScreen.prototype.port3C5_read = function() VGAScreen.prototype.port3C6_write = function(data) { - this.dac_mask = data; + if(this.dac_mask !== data) + { + this.dac_mask = data; + this.complete_redraw(); + } }; VGAScreen.prototype.port3C6_read = function() @@ -1923,6 +1878,8 @@ VGAScreen.prototype.port3D5_write = function(value) this.update_cursor_scanline(); this.update_layers(); + + this.set_font_bitmap(false); break; case 0xA: dbg_log("3D5 / cursor scanline start write: " + h(value), LOG_VGA); @@ -2495,19 +2452,9 @@ VGAScreen.prototype.screen_fill_buffer = function() if(!this.graphical_mode) { // text mode - if(this.graphical_text) - { - const image_data = this.graphical_text.render(); - this.screen.update_buffer([{ - image_data: image_data, - screen_x: 0, - screen_y: 0, - buffer_x: 0, - buffer_y: 0, - buffer_width: image_data.width, - buffer_height: image_data.height - }]); - } + // Update retrace behaviour anyway - programs waiting for signal before + // changing to graphical mode + this.update_vertical_retrace(); return; } @@ -2570,3 +2517,35 @@ VGAScreen.prototype.screen_fill_buffer = function() this.reset_diffs(); this.update_vertical_retrace(); }; + +VGAScreen.prototype.set_font_bitmap = function(font_plane_dirty) +{ + const height = this.max_scan_line & 0x1f; + if(height && !this.graphical_mode) + { + const width_dbl = !!(this.clocking_mode & 0x08); + const width_9px = !width_dbl && !(this.clocking_mode & 0x01); + const copy_8th_col = !!(this.attribute_mode & 0x04); + this.screen.set_font_bitmap( + height + 1, // int height, font height 1..32px + width_9px, // bool width_9px, True: font width 9px, else 8px + width_dbl, // bool width_dbl, True: font width 16px (overrides width_9px) + copy_8th_col, // bool copy_8th_col, True: duplicate 8th into 9th column in ASCII chars 0xC0-0xDF + this.plane2, // Uint8Array font_bitmap[64k], static + font_plane_dirty // bool bitmap_changed, True: content of this.plane2 has changed + ); + } +}; + +VGAScreen.prototype.set_font_page = function() +{ + // bits 2, 3 and 5 (LSB to MSB): VGA font page index of font A + // bits 0, 1 and 4: VGA font page index of font B + // linear_index_map[] maps VGA's non-liner font page index to linear index + const linear_index_map = [0, 2, 4, 6, 1, 3, 5, 7]; + const vga_index_A = ((this.character_map_select & 0b1100) >> 2) | ((this.character_map_select & 0b100000) >> 3); + const vga_index_B = (this.character_map_select & 0b11) | ((this.character_map_select & 0b10000) >> 2); + this.font_page_ab_enabled = vga_index_A !== vga_index_B; + this.screen.set_font_page(linear_index_map[vga_index_A], linear_index_map[vga_index_B]); + this.complete_redraw(); +}; diff --git a/src/vga_text.js b/src/vga_text.js deleted file mode 100644 index 5b5949b038..0000000000 --- a/src/vga_text.js +++ /dev/null @@ -1,628 +0,0 @@ -/* -vga_text.js - -Renders text to image buffer using VGA fonts and attributes. -*/ -"use strict"; - -/** - * @constructor - * @param {VGAScreen} vga - */ -function GraphicalText(vga) -{ - this.vga = vga; - - /** - * Number of text columns - * @type {number} - */ - this.txt_width = 80; - - /** - * Number of text rows - * @type {number} - */ - this.txt_height = 25; - - /** - * If true then at least one row in txt_row_dirty is marked as modified - * @type{number} - */ - this.txt_dirty = 0; - - /** - * One bool per row, row was modified if its entry is != 0 - */ - this.txt_row_dirty = new Uint8Array(this.txt_height); - - /** - * Font bitmaps in VGA memory were changed if true - * @type{boolean} - */ - this.font_data_dirty = false; - - /** - * Font width in pixel (8, 9 or 16) - * @type {number} - */ - this.font_width = 9; - - /** - * Font height in pixel (0...32) - * @type {number} - */ - this.font_height = 16; - - /** - * Duplicate 8th to 9th column in horizontal line drawing characters if true (Line Graphics Enable) - * @type{boolean} - */ - this.font_lge = false; - - /** - * Flat bitmap of 8 fonts, array of size: 8 * 256 * font_width * font_height - * @type{Uint8ClampedArray} - */ - this.font_bitmap = new Uint8ClampedArray(8 * 256 * this.font_width * this.font_height); - - /** - * True: blink when msb (0x80) of text attribute is set (8 background colors) - * False: msb selects background intensity (16 background colors) - * @type{boolean} - */ - this.font_blink_enabled = false; - - /** - * Active index (0...7) of font A - * @type {number} - */ - this.font_index_A = 0; - - /** - * Active index (0...7) of font B (TODO) - * @type {number} - */ - this.font_index_B = 0; - - /** - * If true then cursor_enabled_latch, cursor_top_latch and cursor_bottom_latch were overwritten since last call to render(). - * @type{boolean} - */ - this.cursor_attr_dirty = false; - - /** - * Latest value for cursor_enabled if cursor_attr_dirty is true - * @type{boolean} - */ - this.cursor_enabled_latch = false; - - /** - * Latest value for cursor_top_latch if cursor_attr_dirty is true - * @type {number} - */ - this.cursor_top_latch = 0; - - /** - * Latest value for cursor_bottom_latch if cursor_attr_dirty is true - * @type {number} - */ - this.cursor_bottom_latch = 0; - - /** - * If true then cursor_row_latch and cursor_col_latch were overwritten since last call to render(). - * @type{boolean} - */ - this.cursor_pos_dirty = false; - - /** - * Latest value for cursor_row if cursor_pos_dirty is true - * @type {number} - */ - this.cursor_row_latch = 0; - - /** - * Latest value for cursor_col if cursor_pos_dirty is true - * @type {number} - */ - this.cursor_col_latch = 0; - - /** - * Emulate cursor if true, else disable cursor - * @type{boolean} - */ - this.cursor_enabled = false; - - /** - * Cursor position's row (0...txt_height-1) - * @type {number} - */ - this.cursor_row = 0; - - /** - * Cursor position's column (0...txt_width-1) - * @type {number} - */ - this.cursor_col = 0; - - /** - * Cursor box's top scanline (0...font_height) - * @type {number} - */ - this.cursor_top = 0; - - /** - * Cursor box's bottom scanline (0...font_height, inclusive) - * @type {number} - */ - this.cursor_bottom = 0; - - /** - * Tracked value of register vga.attribute_mode - * @type {number} - */ - this.vga_attribute_mode = 0; - - /** - * Tracked value of register vga.clocking_mode - * @type {number} - */ - this.vga_clocking_mode = 0; - - /** - * Tracked value of register vga.max_scan_line - * @type {number} - */ - this.vga_max_scan_line = 0; - - /** - * Width of graphics canvas in pixel (txt_width * font_width) - * @type {number} - */ - this.gfx_width = this.txt_width * this.font_width; - - /** - * Height of graphics canvas in pixel (txt_height * font_height) - * @type {number} - */ - this.gfx_height = this.txt_height * this.font_height; - - /** - * Local screen bitmap buffer, array of size: gfx_width * gfx_height * 4 - * @type{Uint8ClampedArray} - */ - this.gfx_data = new Uint8ClampedArray(this.gfx_width * this.gfx_height * 4); - - /** - * Image container of local screen bitmap buffer gfx_data - * @type{ImageData} - */ - this.image_data = new ImageData(this.gfx_data, this.gfx_width, this.gfx_height); - - /** - * Show cursor and blinking text now if true (controlled by framerate counter) - * @type{boolean} - */ - this.blink_visible = false; - - /** - * Frame counter to control blink rate of type Uint32 - * @type {number} - */ - this.frame_count = 0; -} - -GraphicalText.prototype.rebuild_font_bitmap = function(width_9px, width_double) -{ - const font_height = this.font_height; - const font_lge = this.font_lge; - const src_bitmap = this.vga.plane2; - const dst_bitmap = new Uint8ClampedArray(8 * 256 * this.font_width * font_height); - const vga_inc_chr = 32 - font_height; - - let i_dst = 0; - const copy_bit = width_double ? - function(value) - { - dst_bitmap[i_dst++] = value; - dst_bitmap[i_dst++] = value; - } : - function(value) - { - dst_bitmap[i_dst++] = value; - }; - - let i_src = 0; - for(let i_font = 0; i_font < 8; ++i_font) - { - for(let i_chr = 0; i_chr < 256; ++i_chr, i_src += vga_inc_chr) - { - for(let i_line = 0; i_line < font_height; ++i_line) - { - const line_bits = src_bitmap[i_src++]; - for(let i_bit = 0x80; i_bit > 0; i_bit >>= 1) - { - copy_bit(line_bits & i_bit ? 1 : 0); - } - if(width_9px) - { - copy_bit(font_lge && i_chr >= 0xC0 && i_chr <= 0xDF && line_bits & 1 ? 1 : 0); - } - } - } - } - - return dst_bitmap; -}; - -GraphicalText.prototype.resize_canvas = function() -{ - this.txt_dirty = 1; - this.txt_row_dirty.fill(1); -}; - -GraphicalText.prototype.rebuild_image_data = function() -{ - const gfx_size = this.gfx_width * this.gfx_height * 4; - const gfx_data = new Uint8ClampedArray(gfx_size); - for(let i = 3; i < gfx_size; i += 4) - { - gfx_data[i] = 0xff; - } - this.gfx_data = gfx_data; - this.image_data = new ImageData(this.gfx_data, this.gfx_width, this.gfx_height); - this.resize_canvas(); -}; - -GraphicalText.prototype.mark_blinking_rows_dirty = function() -{ - const vga_memory = this.vga.vga_memory; - const txt_row_dirty = this.txt_row_dirty; - const txt_width = this.txt_width; - const txt_height = this.txt_height; - const txt_row_size = txt_width * 2; - const txt_row_step = Math.max(0, (this.vga.offset_register * 2 - txt_width) * 2); - const split_screen_row = this.vga.scan_line_to_screen_row(this.vga.line_compare); - let row, col, txt_i = this.vga.start_address << 1; - - for(row = 0; row < txt_height; ++row, txt_i += txt_row_step) - { - if(row === split_screen_row) - { - txt_i = 0; - } - - if(txt_row_dirty[row]) - { - txt_i += txt_row_size; - continue; - } - - for(col = 0; col < txt_width; ++col, txt_i += 2) - { - if(vga_memory[txt_i | 1] & 0x80) - { - txt_row_dirty[row] = this.txt_dirty = 1; - txt_i += txt_row_size - col * 2; - break; - } - } - } -}; - -GraphicalText.prototype.render_dirty_rows = function() -{ - const vga = this.vga; - const vga_memory = vga.vga_memory; - const txt_width = this.txt_width; - const txt_height = this.txt_height; - const txt_row_dirty = this.txt_row_dirty; - const gfx_data = this.gfx_data; - const font_bitmap = this.font_bitmap; - const font_size = this.font_width * this.font_height; - const font_A_offset = this.font_index_A * 256; - const font_B_offset = this.font_index_B * 256; - const font_AB_enabled = font_A_offset !== font_B_offset; - const font_blink_enabled = this.font_blink_enabled; - //const blink_visible = this.blink_visible; - const blink_visible = true; - const cursor_visible = this.cursor_enabled && blink_visible; - const cursor_top = this.cursor_top; - const cursor_height = this.cursor_bottom - cursor_top + 1; - - const split_screen_row = vga.scan_line_to_screen_row(vga.line_compare); - const bg_color_mask = font_blink_enabled ? 0x7 : 0xF; - const palette = new Int32Array(16); - for(let i = 0; i < 16; ++i) - { - palette[i] = vga.vga256_palette[vga.dac_mask & vga.dac_map[i]]; - } - - const txt_row_size = txt_width * 2; - const txt_row_step = Math.max(0, (vga.offset_register * 2 - txt_width) * 2); - - const gfx_col_size = this.font_width * 4; // column size in gfx_data (tuple of 4 RGBA items) - const gfx_line_size = this.gfx_width * 4; // line size in gfx_data - const gfx_row_size = gfx_line_size * this.font_height; // row size in gfx_data - const gfx_col_step = (this.font_width - this.font_height * this.gfx_width) * 4; // move from end of current column to start of next in gfx_data - const gfx_line_step = (this.gfx_width - this.font_width) * 4; // move forward to start of column's next line in gfx_data - - // int, current cursor linear position in canvas coordinates (top left of row/col) - const cursor_gfx_i = (this.cursor_row * this.gfx_width * this.font_height + this.cursor_col * this.font_width) * 4; - - let txt_i, chr, chr_attr, chr_bg_rgba, chr_fg_rgba, chr_blinking, chr_font_ofs; - let fg, bg, fg_r=0, fg_g=0, fg_b=0, bg_r=0, bg_g=0, bg_b=0; - let gfx_i, gfx_end_y, gfx_end_x, glyph_i; - let draw_cursor, gfx_ic; - let row, col; - - txt_i = vga.start_address << 1; - - for(row = 0; row < txt_height; ++row, txt_i += txt_row_step) - { - if(row === split_screen_row) - { - txt_i = 0; - } - - if(! txt_row_dirty[row]) - { - txt_i += txt_row_size; - continue; - } - - gfx_i = row * gfx_row_size; - - for(col = 0; col < txt_width; ++col, txt_i += 2, gfx_i += gfx_col_step) - { - chr = vga_memory[txt_i]; - chr_attr = vga_memory[txt_i | 1]; - chr_blinking = font_blink_enabled && chr_attr & 0x80; - chr_font_ofs = font_AB_enabled ? (chr_attr & 0x8 ? font_A_offset : font_B_offset) : font_A_offset; - chr_bg_rgba = palette[chr_attr >> 4 & bg_color_mask]; - chr_fg_rgba = palette[chr_attr & 0xF]; - - if(bg !== chr_bg_rgba) - { - bg = chr_bg_rgba; - bg_r = bg >> 16; - bg_g = (bg >> 8) & 0xff; - bg_b = bg & 0xff; - } - - if(chr_blinking && ! blink_visible) - { - if(fg !== bg) { - fg = bg; - fg_r = bg_r; - fg_g = bg_g; - fg_b = bg_b; - } - } - else if(fg !== chr_fg_rgba) - { - fg = chr_fg_rgba; - fg_r = fg >> 16; - fg_g = (fg >> 8) & 0xff; - fg_b = fg & 0xff; - } - - draw_cursor = cursor_visible && cursor_gfx_i === gfx_i; - - glyph_i = (chr_font_ofs + chr) * font_size; - - gfx_end_y = gfx_i + gfx_row_size; - for(; gfx_i < gfx_end_y; gfx_i += gfx_line_step) - { - gfx_end_x = gfx_i + gfx_col_size; - for(; gfx_i < gfx_end_x; gfx_i += 4) - { - if(font_bitmap[glyph_i++]) - { - gfx_data[gfx_i] = fg_r; - gfx_data[gfx_i+1] = fg_g; - gfx_data[gfx_i+2] = fg_b; - } - else - { - gfx_data[gfx_i] = bg_r; - gfx_data[gfx_i+1] = bg_g; - gfx_data[gfx_i+2] = bg_b; - } - } - } - - if(draw_cursor) - { - gfx_ic = cursor_gfx_i + cursor_top * gfx_line_size; - gfx_end_y = gfx_ic + cursor_height * gfx_line_size; - for(; gfx_ic < gfx_end_y; gfx_ic += gfx_line_step) - { - gfx_end_x = gfx_ic + gfx_col_size; - for(; gfx_ic < gfx_end_x; gfx_ic += 4) - { - gfx_data[gfx_ic] = fg_r; - gfx_data[gfx_ic+1] = fg_g; - gfx_data[gfx_ic+2] = fg_b; - } - } - } - } - } -}; - -// -// Public methods -// - -GraphicalText.prototype.mark_dirty = function() -{ - this.txt_row_dirty.fill(1); - this.txt_dirty = 1; -}; - -GraphicalText.prototype.invalidate_row = function(row) -{ - if(row >= 0 && row < this.txt_height) - { - this.txt_row_dirty[row] = this.txt_dirty = 1; - } -}; - -GraphicalText.prototype.invalidate_font_shape = function() -{ - this.font_data_dirty = true; -}; - -GraphicalText.prototype.set_size = function(rows, cols) -{ - if(rows > 0 && rows < 256 && cols > 0 && cols < 256) - { - this.txt_width = cols; - this.txt_height = rows; - - this.gfx_width = this.txt_width * this.font_width; - this.gfx_height = this.txt_height * this.font_height; - - this.txt_row_dirty = new Uint8Array(this.txt_height); - this.vga.screen.set_size_graphical(this.gfx_width, this.gfx_height, this.gfx_width, this.gfx_height); - this.mark_dirty(); - this.rebuild_image_data(); - } -}; - -GraphicalText.prototype.set_character_map = function(char_map_select) -{ - // bits 2, 3 and 5 (LSB to MSB): VGA font page index of font A - // bits 0, 1 and 4: VGA font page index of font B - // linear_index_map[] maps VGA's non-liner font page index to linear index - const linear_index_map = [0, 2, 4, 6, 1, 3, 5, 7]; - const vga_index_A = ((char_map_select & 0b1100) >> 2) | ((char_map_select & 0b100000) >> 3); - const vga_index_B = (char_map_select & 0b11) | ((char_map_select & 0b10000) >> 2); - const font_index_A = linear_index_map[vga_index_A]; - const font_index_B = linear_index_map[vga_index_B]; - - if(this.font_index_A !== font_index_A || this.font_index_B !== font_index_B) - { - this.font_index_A = font_index_A; - this.font_index_B = font_index_B; - this.mark_dirty(); - } -}; - -GraphicalText.prototype.set_cursor_pos = function(row, col) -{ - this.cursor_pos_dirty = true; - this.cursor_row_latch = row; - this.cursor_col_latch = col; -}; - -GraphicalText.prototype.set_cursor_attr = function(start, end, visible) -{ - this.cursor_attr_dirty = true; - this.cursor_enabled_latch = !! visible; - this.cursor_top_latch = start; - this.cursor_bottom_latch = end; -}; - -GraphicalText.prototype.render = function() -{ - // increment Uint32 frame counter - this.frame_count = (this.frame_count + 1) >>> 0; - - // apply changes to font_width, font_height, font_lge, font_bitmap and font_blink_enabled - const curr_clocking_mode = this.vga.clocking_mode & 0b00001001; - const curr_attribute_mode = this.vga.attribute_mode & 0b00001100; - const curr_max_scan_line = this.vga.max_scan_line & 0b10011111; - if(this.font_data_dirty || - this.vga_clocking_mode !== curr_clocking_mode || - this.vga_attribute_mode !== curr_attribute_mode || - this.vga_max_scan_line !== curr_max_scan_line) - { - const width_9px = ! (curr_clocking_mode & 0x01); - const width_double = !! (curr_clocking_mode & 0x08); - const curr_font_width = (width_9px ? 9 : 8) * (width_double ? 2 : 1); - const curr_font_blink_enabled = !! (curr_attribute_mode & 0b00001000); - const curr_font_lge = !! (curr_attribute_mode & 0b00000100); - const curr_font_height = (curr_max_scan_line & 0b00011111) + 1; - - const font_data_changed = this.font_data_dirty || this.font_lge !== curr_font_lge; - const font_size_changed = this.font_width !== curr_font_width || this.font_height !== curr_font_height; - - this.font_data_dirty = false; - this.font_width = curr_font_width; - this.font_height = curr_font_height; - this.font_blink_enabled = curr_font_blink_enabled; - this.font_lge = curr_font_lge; - - this.vga_clocking_mode = curr_clocking_mode; - this.vga_attribute_mode = curr_attribute_mode; - this.vga_max_scan_line = curr_max_scan_line; - - if(font_data_changed || font_size_changed) - { - if(font_size_changed) - { - this.gfx_width = this.txt_width * this.font_width; - this.gfx_height = this.txt_height * this.font_height; - this.rebuild_image_data(); - } - this.font_bitmap = this.rebuild_font_bitmap(width_9px, width_double); - } - this.mark_dirty(); - } - - // apply changes to cursor position - if(this.cursor_pos_dirty) - { - this.cursor_pos_dirty = false; - this.cursor_row_latch = Math.min(this.cursor_row_latch, this.txt_height-1); - this.cursor_col_latch = Math.min(this.cursor_col_latch, this.txt_width-1); - if(this.cursor_row !== this.cursor_row_latch || this.cursor_col !== this.cursor_col_latch) - { - this.txt_row_dirty[this.cursor_row] = this.txt_row_dirty[this.cursor_row_latch] = this.txt_dirty = 1; - this.cursor_row = this.cursor_row_latch; - this.cursor_col = this.cursor_col_latch; - } - } - - // apply changes to cursor_enabled, cursor_top and cursor_bottom - if(this.cursor_attr_dirty) - { - this.cursor_attr_dirty = false; - if(this.cursor_enabled !== this.cursor_enabled_latch || - this.cursor_top !== this.cursor_top_latch || - this.cursor_bottom !== this.cursor_bottom_latch) - { - this.cursor_enabled = this.cursor_enabled_latch; - this.cursor_top = this.cursor_top_latch; - this.cursor_bottom = this.cursor_bottom_latch; - this.txt_row_dirty[this.cursor_row] = this.txt_dirty = 1; - } - } - - // toggle cursor and blinking character visibility at a frequency of ~3.75hz (every 16th frame at 60fps) - // TODO: make framerate independant - //if(this.frame_count % 16 === 0) - //{ - // this.blink_visible = ! this.blink_visible; - // if(this.font_blink_enabled) - // { - // this.mark_blinking_rows_dirty(); - // } - // if(this.cursor_enabled) - // { - // this.txt_row_dirty[this.cursor_row] = this.txt_dirty = 1; - // } - //} - - // render changed rows - if(this.txt_dirty) - { - this.render_dirty_rows(); - this.txt_dirty = 0; - this.txt_row_dirty.fill(0); - } - - return this.image_data; -}; From 12d71fe081e77417bc2de18f80f6d34af83e4a80 Mon Sep 17 00:00:00 2001 From: Fabian Date: Mon, 23 Sep 2024 12:45:08 -0600 Subject: [PATCH 06/90] log error when aborting http request --- src/lib.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.js b/src/lib.js index f9cf669cb6..4b322c2e6e 100644 --- a/src/lib.js +++ b/src/lib.js @@ -581,6 +581,7 @@ function load_file(filename, options, n_tries) { if(http.status === 200) { + console.error("Server sent full file in response to ranged request, aborting", { filename }); http.abort(); } }; From 5c308719ef8ff7c31aea87fd04cfa48824ddaaa7 Mon Sep 17 00:00:00 2001 From: Fabian Date: Mon, 23 Sep 2024 13:49:49 -0600 Subject: [PATCH 07/90] update haiku thanks to @SuperMaxusa for providing this image --- src/browser/main.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/browser/main.js b/src/browser/main.js index bdabbb2256..54471f7357 100644 --- a/src/browser/main.js +++ b/src/browser/main.js @@ -272,13 +272,13 @@ id: "haiku", memory_size: 512 * 1024 * 1024, hda: { - url: host + "haiku-v3/.img", + url: host + "haiku-v4/.img", size: 1 * 1024 * 1024 * 1024, async: true, fixed_chunk_size: 1024 * 1024, use_parts: true, }, - state: { url: host + "haiku_state-v3.bin.zst" }, + state: { url: host + "haiku_state-v4.bin.zst" }, name: "Haiku", homepage: "https://www.haiku-os.org/", }, @@ -286,7 +286,7 @@ id: "haiku-boot", memory_size: 512 * 1024 * 1024, hda: { - url: host + "haiku-v3/.img", + url: host + "haiku-v4/.img", size: 1 * 1024 * 1024 * 1024, async: true, fixed_chunk_size: 1024 * 1024, From 58f32c705ddbf6bbbca51eb8ee2ee58844293882 Mon Sep 17 00:00:00 2001 From: Fabian Date: Mon, 23 Sep 2024 15:07:39 -0600 Subject: [PATCH 08/90] update ms-dos image with cd-rom driver, various tools and compilers thanks to @SuperMaxusa for providing the image --- src/browser/main.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/browser/main.js b/src/browser/main.js index 54471f7357..6000c06a1c 100644 --- a/src/browser/main.js +++ b/src/browser/main.js @@ -298,9 +298,19 @@ { id: "msdos", hda: { - url: host + "msdos.img", - size: 8 * 1024 * 1024, - async: false, + url: host + "msdos622/.img", + size: 64 * 1024 * 1024, + async: true, + fixed_chunk_size: 256 * 1024, + use_parts: true, + }, + name: "MS-DOS", + }, + { + id: "msdos4", + fda: { + url: host + "msdos4.img", + size: 1474560, }, name: "MS-DOS", }, From 8c233032412475e091190488f6f18e70f2c7ae2c Mon Sep 17 00:00:00 2001 From: Fabian Date: Mon, 23 Sep 2024 16:02:59 -0600 Subject: [PATCH 09/90] update redox --- src/browser/main.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/browser/main.js b/src/browser/main.js index 6000c06a1c..e02af052cb 100644 --- a/src/browser/main.js +++ b/src/browser/main.js @@ -217,14 +217,14 @@ id: "redox", name: "Redox", hda: { - url: host + "redox_demo_i686_2022-11-26_643_harddrive/.img", - size: 512 * 1024 * 1024, + url: host + "redox_demo_i686_2024-09-07_1225_harddrive/.img", + size: 671088640, async: true, fixed_chunk_size: 1024 * 1024, use_parts: true, }, - memory_size: 512 * 1024 * 1024, - state: { url: host + "redox_state.bin.zst" }, + memory_size: 1024 * 1024 * 1024, + state: { url: host + "redox_state-v2.bin.zst" }, homepage: "https://www.redox-os.org/", acpi: true, }, @@ -232,13 +232,13 @@ id: "redox-boot", name: "Redox", hda: { - url: host + "redox_demo_i686_2022-11-26_643_harddrive/.img", - size: 512 * 1024 * 1024, + url: host + "redox_demo_i686_2024-09-07_1225_harddrive/.img", + size: 671088640, async: true, fixed_chunk_size: 1024 * 1024, use_parts: true, }, - memory_size: 512 * 1024 * 1024, + memory_size: 1024 * 1024 * 1024, homepage: "https://www.redox-os.org/", acpi: true, }, From 164c368e4c2df41a67fec92f9af312436c84f853 Mon Sep 17 00:00:00 2001 From: Fabian Date: Mon, 23 Sep 2024 16:12:41 -0600 Subject: [PATCH 10/90] update arch state --- src/browser/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/main.js b/src/browser/main.js index e02af052cb..50481a63d2 100644 --- a/src/browser/main.js +++ b/src/browser/main.js @@ -145,7 +145,7 @@ name: "Arch Linux", memory_size: 512 * 1024 * 1024, vga_memory_size: 8 * 1024 * 1024, - state: { url: host + "arch_state.bin.zst" }, + state: { url: host + "arch_state-v2.bin.zst" }, filesystem: { baseurl: host + "arch/", }, From 56115c90c450bb41265f8b836c8f245360cf16ed Mon Sep 17 00:00:00 2001 From: Fabian Date: Sat, 28 Sep 2024 16:34:20 -0600 Subject: [PATCH 11/90] fix truncation bug in ldt segment limit check (was wrapping 0x10000 to 0, causing all segment register loads in the ldt to trigger #gp) --- src/rust/cpu/cpu.rs | 68 ++++++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/src/rust/cpu/cpu.rs b/src/rust/cpu/cpu.rs index 36c4c971b9..20536ddef2 100644 --- a/src/rust/cpu/cpu.rs +++ b/src/rust/cpu/cpu.rs @@ -2373,16 +2373,23 @@ pub unsafe fn lookup_segment_selector( } let (table_offset, table_limit) = if selector.is_gdt() { - (*gdtr_offset as u32, *gdtr_size as u16) + (*gdtr_offset as u32, *gdtr_size as u32) } else { ( *segment_offsets.offset(LDTR as isize) as u32, - *segment_limits.offset(LDTR as isize) as u16, + *segment_limits.offset(LDTR as isize) as u32, ) }; - if selector.descriptor_offset() > table_limit { + if selector.descriptor_offset() as u32 > table_limit { + dbg_log!( + "segment outside of table limit: selector={:x} offset={:x} isgdt={} table_limit={:x}", + selector.raw, + selector.descriptor_offset(), + selector.is_gdt(), + table_limit + ); return Ok(Err(SelectorNullOrInvalid::OutsideOfTableLimit)); } @@ -2421,36 +2428,28 @@ pub unsafe fn switch_seg(reg: i32, selector_raw: i32) -> bool { let selector = SegmentSelector::of_u16(selector_raw as u16); let descriptor = match return_on_pagefault!(lookup_segment_selector(selector), false) { Ok((desc, _)) => desc, - Err(selector_unusable) => { - // The selector couldn't be used to fetch a descriptor, so we handle all of those - // cases - if selector_unusable == SelectorNullOrInvalid::IsNull { - if reg == SS { - dbg_log!("#GP for loading 0 in SS sel={:x}", selector_raw); - trigger_gp(0); - return false; - } - else if reg != CS { - // es, ds, fs, gs - *sreg.offset(reg as isize) = selector_raw as u16; - *segment_is_null.offset(reg as isize) = true; - update_state_flags(); - return true; - } - } - else if selector_unusable == SelectorNullOrInvalid::OutsideOfTableLimit { - dbg_log!( - "#GP for loading invalid in seg={} sel={:x} gdt_limit={:x}", - reg, - selector_raw, - *gdtr_size, - ); - dbg_trace(); - trigger_gp(selector_raw & !3); + Err(SelectorNullOrInvalid::IsNull) => { + if reg == SS { + dbg_log!("#GP for loading 0 in SS sel={:x}", selector_raw); + trigger_gp(0); return false; } - - dbg_assert!(false); + else { + // es, ds, fs, gs + *sreg.offset(reg as isize) = selector_raw as u16; + *segment_is_null.offset(reg as isize) = true; + update_state_flags(); + return true; + } + } + Err(SelectorNullOrInvalid::OutsideOfTableLimit) => { + dbg_log!( + "#GP for loading invalid in seg={} sel={:x}", + reg, + selector_raw, + ); + dbg_trace(); + trigger_gp(selector_raw & !3); return false; }, }; @@ -2580,6 +2579,7 @@ pub unsafe fn load_ldt(selector: i32) -> OrPageFault<()> { let selector = SegmentSelector::of_u16(selector as u16); if selector.is_null() { + dbg_log!("lldt: null loaded"); *segment_limits.offset(LDTR as isize) = 0; *segment_offsets.offset(LDTR as isize) = 0; *sreg.offset(LDTR as isize) = selector.raw; @@ -2613,6 +2613,12 @@ pub unsafe fn load_ldt(selector: i32) -> OrPageFault<()> { ); } + dbg_log!( + "lldt: {:x} offset={:x} limit={:x}", + selector.raw, + descriptor.base(), + descriptor.effective_limit() + ); *segment_limits.offset(LDTR as isize) = descriptor.effective_limit(); *segment_offsets.offset(LDTR as isize) = descriptor.base(); *sreg.offset(LDTR as isize) = selector.raw; From fa6c540b479ede13cdd53ca42e24c007920e39c2 Mon Sep 17 00:00:00 2001 From: Fabian Date: Tue, 1 Oct 2024 18:53:04 -0600 Subject: [PATCH 12/90] set accessed bit on segments descriptors in gdt/ldt --- src/rust/cpu/cpu.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/rust/cpu/cpu.rs b/src/rust/cpu/cpu.rs index 20536ddef2..7970f9d7c4 100644 --- a/src/rust/cpu/cpu.rs +++ b/src/rust/cpu/cpu.rs @@ -385,6 +385,7 @@ impl SegmentDescriptor { pub fn is_system(&self) -> bool { self.access_byte() & 0x10 == 0 } pub fn system_type(&self) -> u8 { self.access_byte() & 0xF } + pub fn accessed(&self) -> bool { self.access_byte() & 1 == 1 } pub fn is_rw(&self) -> bool { self.access_byte() & 2 == 2 } pub fn is_dc(&self) -> bool { self.access_byte() & 4 == 4 } pub fn is_executable(&self) -> bool { self.access_byte() & 8 == 8 } @@ -407,6 +408,11 @@ impl SegmentDescriptor { raw: self.raw | 2 << 40, } } + pub fn set_accessed(&self) -> SegmentDescriptor { + SegmentDescriptor { + raw: self.raw | 1 << 40, + } + } } pub struct InterruptDescriptor { @@ -2426,8 +2432,8 @@ pub unsafe fn switch_seg(reg: i32, selector_raw: i32) -> bool { } let selector = SegmentSelector::of_u16(selector_raw as u16); - let descriptor = match return_on_pagefault!(lookup_segment_selector(selector), false) { - Ok((desc, _)) => desc, + let (mut descriptor, descriptor_address) = match return_on_pagefault!(lookup_segment_selector(selector), false) { + Ok(desc) => desc, Err(SelectorNullOrInvalid::IsNull) => { if reg == SS { dbg_log!("#GP for loading 0 in SS sel={:x}", selector_raw); @@ -2513,6 +2519,15 @@ pub unsafe fn switch_seg(reg: i32, selector_raw: i32) -> bool { } } + if !descriptor.accessed() { + descriptor = descriptor.set_accessed(); + + memory::write8( + translate_address_system_write(descriptor_address + 5).unwrap(), + descriptor.access_byte() as i32, + ); + } + *segment_is_null.offset(reg as isize) = false; *segment_limits.offset(reg as isize) = descriptor.effective_limit(); *segment_offsets.offset(reg as isize) = descriptor.base(); From 1f8ede44ef75a99d3ad034fa74ab1c1cb7dfebe7 Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 2 Oct 2024 13:44:04 -0600 Subject: [PATCH 13/90] minor --- src/cpu.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cpu.js b/src/cpu.js index 917fd1b2f0..0a79376d6a 100644 --- a/src/cpu.js +++ b/src/cpu.js @@ -1443,6 +1443,8 @@ CPU.prototype.load_bios = function() return; } + dbg_assert(bios instanceof ArrayBuffer); + // load bios var data = new Uint8Array(bios), start = 0x100000 - bios.byteLength; @@ -1451,6 +1453,8 @@ CPU.prototype.load_bios = function() if(vga_bios) { + dbg_assert(vga_bios instanceof ArrayBuffer); + // load vga bios var vga_bios8 = new Uint8Array(vga_bios); From 7619c4482587eeebc3d570e97aa1703f2ee8caaf Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 2 Oct 2024 15:27:25 -0600 Subject: [PATCH 14/90] minor refactoring --- src/rust/cpu/cpu.rs | 122 +++++++++++++++++++------------------------- 1 file changed, 53 insertions(+), 69 deletions(-) diff --git a/src/rust/cpu/cpu.rs b/src/rust/cpu/cpu.rs index 7970f9d7c4..83aba25c73 100644 --- a/src/rust/cpu/cpu.rs +++ b/src/rust/cpu/cpu.rs @@ -592,13 +592,9 @@ pub unsafe fn iret(is_16: bool) { let cs_selector = SegmentSelector::of_u16(new_cs as u16); let cs_descriptor = match return_on_pagefault!(lookup_segment_selector(cs_selector)) { Ok((desc, _)) => desc, - Err(selector_unusable) => match selector_unusable { - SelectorNullOrInvalid::IsNull => { - panic!("Unimplemented: CS selector is null"); - }, - SelectorNullOrInvalid::OutsideOfTableLimit => { - panic!("Unimplemented: CS selector is invalid"); - }, + Err(SelectorNullOrInvalid::IsNull) => panic!("Unimplemented: CS selector is null"), + Err(SelectorNullOrInvalid::OutsideOfTableLimit) => { + panic!("Unimplemented: CS selector is invalid") }, }; @@ -653,18 +649,16 @@ pub unsafe fn iret(is_16: bool) { let ss_selector = SegmentSelector::of_u16(temp_ss as u16); let ss_descriptor = match return_on_pagefault!(lookup_segment_selector(ss_selector)) { Ok((desc, _)) => desc, - Err(selector_unusable) => match selector_unusable { - SelectorNullOrInvalid::IsNull => { - dbg_log!("#GP for loading 0 in SS sel={:x}", temp_ss); - dbg_trace(); - trigger_gp(0); - return; - }, - SelectorNullOrInvalid::OutsideOfTableLimit => { - dbg_log!("#GP for loading invalid in SS sel={:x}", temp_ss); - trigger_gp(temp_ss & !3); - return; - }, + Err(SelectorNullOrInvalid::IsNull) => { + dbg_log!("#GP for loading 0 in SS sel={:x}", temp_ss); + dbg_trace(); + trigger_gp(0); + return; + }, + Err(SelectorNullOrInvalid::OutsideOfTableLimit) => { + dbg_log!("#GP for loading invalid in SS sel={:x}", temp_ss); + trigger_gp(temp_ss & !3); + return; }, }; let new_cpl = cs_selector.rpl(); @@ -858,15 +852,13 @@ pub unsafe fn call_interrupt_vector( SegmentSelector::of_u16(selector as u16) )) { Ok((desc, _)) => desc, - Err(selector_unusable) => match selector_unusable { - SelectorNullOrInvalid::IsNull => { - dbg_log!("is null"); - panic!("Unimplemented: #GP handler"); - }, - SelectorNullOrInvalid::OutsideOfTableLimit => { - dbg_log!("is invalid"); - panic!("Unimplemented: #GP handler (error code)"); - }, + Err(SelectorNullOrInvalid::IsNull) => { + dbg_log!("is null"); + panic!("Unimplemented: #GP handler"); + }, + Err(SelectorNullOrInvalid::OutsideOfTableLimit) => { + dbg_log!("is invalid"); + panic!("Unimplemented: #GP handler (error code)"); }, }; @@ -1119,17 +1111,15 @@ pub unsafe fn far_jump(eip: i32, selector: i32, is_call: bool, is_osize_32: bool let cs_selector = SegmentSelector::of_u16(selector as u16); let info = match return_on_pagefault!(lookup_segment_selector(cs_selector)) { Ok((desc, _)) => desc, - Err(selector_unusable) => match selector_unusable { - SelectorNullOrInvalid::IsNull => { - dbg_log!("#gp null cs"); - trigger_gp(0); - return; - }, - SelectorNullOrInvalid::OutsideOfTableLimit => { - dbg_log!("#gp invalid cs: {:x}", selector); - trigger_gp(selector & !3); - return; - }, + Err(SelectorNullOrInvalid::IsNull) => { + dbg_log!("#gp null cs"); + trigger_gp(0); + return; + }, + Err(SelectorNullOrInvalid::OutsideOfTableLimit) => { + dbg_log!("#gp invalid cs: {:x}", selector); + trigger_gp(selector & !3); + return; }, }; @@ -1160,17 +1150,15 @@ pub unsafe fn far_jump(eip: i32, selector: i32, is_call: bool, is_osize_32: bool SegmentSelector::of_u16(cs_selector as u16) )) { Ok((desc, _)) => desc, - Err(selector_unusable) => match selector_unusable { - SelectorNullOrInvalid::IsNull => { - dbg_log!("#gp null cs"); - trigger_gp(0); - return; - }, - SelectorNullOrInvalid::OutsideOfTableLimit => { - dbg_log!("#gp invalid cs: {:x}", selector); - trigger_gp(selector & !3); - return; - }, + Err(SelectorNullOrInvalid::IsNull) => { + dbg_log!("#gp null cs"); + trigger_gp(0); + return; + }, + Err(SelectorNullOrInvalid::OutsideOfTableLimit) => { + dbg_log!("#gp invalid cs: {:x}", selector); + trigger_gp(selector & !3); + return; }, }; @@ -1204,13 +1192,11 @@ pub unsafe fn far_jump(eip: i32, selector: i32, is_call: bool, is_osize_32: bool let ss_selector = SegmentSelector::of_u16(new_ss as u16); let ss_info = match return_on_pagefault!(lookup_segment_selector(ss_selector)) { Ok((desc, _)) => desc, - Err(selector_unusable) => match selector_unusable { - SelectorNullOrInvalid::IsNull => { - panic!("null ss: {}", new_ss); - }, - SelectorNullOrInvalid::OutsideOfTableLimit => { - panic!("invalid ss: {}", new_ss); - }, + Err(SelectorNullOrInvalid::IsNull) => { + panic!("null ss: {}", new_ss); + }, + Err(SelectorNullOrInvalid::OutsideOfTableLimit) => { + panic!("invalid ss: {}", new_ss); }, }; @@ -1444,17 +1430,15 @@ pub unsafe fn far_return(eip: i32, selector: i32, stack_adjust: i32, is_osize_32 let cs_selector = SegmentSelector::of_u16(selector as u16); let info = match return_on_pagefault!(lookup_segment_selector(cs_selector)) { Ok((desc, _)) => desc, - Err(selector_unusable) => match selector_unusable { - SelectorNullOrInvalid::IsNull => { - dbg_log!("far return: #gp null cs"); - trigger_gp(0); - return; - }, - SelectorNullOrInvalid::OutsideOfTableLimit => { - dbg_log!("far return: #gp invalid cs: {:x}", selector); - trigger_gp(selector & !3); - return; - }, + Err(SelectorNullOrInvalid::IsNull) => { + dbg_log!("far return: #gp null cs"); + trigger_gp(0); + return; + }, + Err(SelectorNullOrInvalid::OutsideOfTableLimit) => { + dbg_log!("far return: #gp invalid cs: {:x}", selector); + trigger_gp(selector & !3); + return; }, }; @@ -2447,7 +2431,7 @@ pub unsafe fn switch_seg(reg: i32, selector_raw: i32) -> bool { update_state_flags(); return true; } - } + }, Err(SelectorNullOrInvalid::OutsideOfTableLimit) => { dbg_log!( "#GP for loading invalid in seg={} sel={:x}", From 8cd6e70c8a89220d548b2e8d66035be9032f53d1 Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 2 Oct 2024 15:28:40 -0600 Subject: [PATCH 15/90] dead code --- src/rust/cpu/cpu.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/rust/cpu/cpu.rs b/src/rust/cpu/cpu.rs index 83aba25c73..6360838d47 100644 --- a/src/rust/cpu/cpu.rs +++ b/src/rust/cpu/cpu.rs @@ -2463,10 +2463,6 @@ pub unsafe fn switch_seg(reg: i32, selector_raw: i32) -> bool { *stack_size_32 = descriptor.is_32(); } - else if reg == CS { - // handled by switch_cs_real_mode, far_return or far_jump - dbg_assert!(false); - } else { if descriptor.is_system() || !descriptor.is_readable() From 0f45aa3ecfb37b086d0338e7d7314f09b118f844 Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 2 Oct 2024 15:44:57 -0600 Subject: [PATCH 16/90] load_tr: use translate_address_system_write for address translating when setting busy flag --- src/rust/cpu/cpu.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/rust/cpu/cpu.rs b/src/rust/cpu/cpu.rs index 6360838d47..18977e2c6d 100644 --- a/src/rust/cpu/cpu.rs +++ b/src/rust/cpu/cpu.rs @@ -2567,7 +2567,10 @@ pub unsafe fn load_tr(selector: i32) { *sreg.offset(TR as isize) = selector.raw; // Mark task as busy - safe_write64(descriptor_address, descriptor.set_busy().raw).unwrap(); + memory::write8( + translate_address_system_write(descriptor_address + 5).unwrap(), + descriptor.set_busy().access_byte() as i32, + ); } pub unsafe fn load_ldt(selector: i32) -> OrPageFault<()> { From ff430be91250804e537d6a11157f98d2ee0005b3 Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 2 Oct 2024 15:45:54 -0600 Subject: [PATCH 17/90] indent --- src/rust/cpu/cpu.rs | 53 +++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/src/rust/cpu/cpu.rs b/src/rust/cpu/cpu.rs index 18977e2c6d..4bfdc829b2 100644 --- a/src/rust/cpu/cpu.rs +++ b/src/rust/cpu/cpu.rs @@ -2416,33 +2416,34 @@ pub unsafe fn switch_seg(reg: i32, selector_raw: i32) -> bool { } let selector = SegmentSelector::of_u16(selector_raw as u16); - let (mut descriptor, descriptor_address) = match return_on_pagefault!(lookup_segment_selector(selector), false) { - Ok(desc) => desc, - Err(SelectorNullOrInvalid::IsNull) => { - if reg == SS { - dbg_log!("#GP for loading 0 in SS sel={:x}", selector_raw); - trigger_gp(0); + let (mut descriptor, descriptor_address) = + match return_on_pagefault!(lookup_segment_selector(selector), false) { + Ok(desc) => desc, + Err(SelectorNullOrInvalid::IsNull) => { + if reg == SS { + dbg_log!("#GP for loading 0 in SS sel={:x}", selector_raw); + trigger_gp(0); + return false; + } + else { + // es, ds, fs, gs + *sreg.offset(reg as isize) = selector_raw as u16; + *segment_is_null.offset(reg as isize) = true; + update_state_flags(); + return true; + } + }, + Err(SelectorNullOrInvalid::OutsideOfTableLimit) => { + dbg_log!( + "#GP for loading invalid in seg={} sel={:x}", + reg, + selector_raw, + ); + dbg_trace(); + trigger_gp(selector_raw & !3); return false; - } - else { - // es, ds, fs, gs - *sreg.offset(reg as isize) = selector_raw as u16; - *segment_is_null.offset(reg as isize) = true; - update_state_flags(); - return true; - } - }, - Err(SelectorNullOrInvalid::OutsideOfTableLimit) => { - dbg_log!( - "#GP for loading invalid in seg={} sel={:x}", - reg, - selector_raw, - ); - dbg_trace(); - trigger_gp(selector_raw & !3); - return false; - }, - }; + }, + }; if reg == SS { if descriptor.is_system() From ca55308e72382b42fda05c6139f50f974093ea15 Mon Sep 17 00:00:00 2001 From: Fabian Date: Sat, 5 Oct 2024 17:27:53 -0600 Subject: [PATCH 18/90] Add Windows ME thanks @SuperMaxusa --- src/browser/main.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/browser/main.js b/src/browser/main.js index 50481a63d2..1684a2f61b 100644 --- a/src/browser/main.js +++ b/src/browser/main.js @@ -741,6 +741,19 @@ }, name: "Windows 2000", }, + { + id: "windows-me", + memory_size: 128 * 1024 * 1024, + hda: { + url: host + "windowsme/.img", + size: 834666496, + async: true, + fixed_chunk_size: 256 * 1024, + use_parts: true, + }, + state: { url: host + "windows-me_state.bin.zst" }, + name: "Windows ME", + }, { id: "windowsnt4", memory_size: 512 * 1024 * 1024, From aeb01fd2b30d15913f6dd27c0d5ce9d7f4af67fd Mon Sep 17 00:00:00 2001 From: Fabian Date: Sat, 5 Oct 2024 17:41:12 -0600 Subject: [PATCH 19/90] add Unix V7 suggested by @SuperMaxusa --- src/browser/main.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/browser/main.js b/src/browser/main.js index 1684a2f61b..9b146f3f1f 100644 --- a/src/browser/main.js +++ b/src/browser/main.js @@ -503,6 +503,17 @@ }, homepage: "https://www.minix3.org/", }, + { + id: "unix-v7", + name: "Unix V7/x86", + hda: { + url: host + "unix-v7x86-0.8a/.img", + size: 152764416, + async: true, + fixed_chunk_size: 256 * 1024, + use_parts: true, + }, + }, { id: "kolibrios", fda: { From 9870cab255416febb6b107c81186001f2d5b57a6 Mon Sep 17 00:00:00 2001 From: Fabian Date: Sat, 5 Oct 2024 17:42:28 -0600 Subject: [PATCH 20/90] add FreeDOS with Xcom desktop suggested by @SuperMaxusa --- src/browser/main.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/browser/main.js b/src/browser/main.js index 9b146f3f1f..8d982624eb 100644 --- a/src/browser/main.js +++ b/src/browser/main.js @@ -333,6 +333,15 @@ }, name: "Freedos with FreeGEM", }, + { + id: "xcom", + fda: { + url: host + "xcom144.img", + size: 1440 * 1024, + }, + name: "Freedos with Xcom", + homepage: "http://xcom.infora.hu/index.html", + }, { id: "psychdos", hda: { From bbfeeb63e03482a815eb8c196949ddae86aee83b Mon Sep 17 00:00:00 2001 From: Fabian Date: Sat, 5 Oct 2024 17:43:47 -0600 Subject: [PATCH 21/90] s/Virtual x86/v86 --- debug.html | 2 +- index.html | 2 +- src/browser/main.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/debug.html b/debug.html index a4e2d20c1a..829aa17ca6 100644 --- a/debug.html +++ b/debug.html @@ -1,7 +1,7 @@ -Virtual x86 (debug) +v86 (debug) diff --git a/index.html b/index.html index ee08215592..cce30e2178 100644 --- a/index.html +++ b/index.html @@ -1,7 +1,7 @@ -Virtual x86 +v86 diff --git a/src/browser/main.js b/src/browser/main.js index 8d982624eb..985ca24a2f 100644 --- a/src/browser/main.js +++ b/src/browser/main.js @@ -11,7 +11,7 @@ function set_title(text) { - document.title = text + " - Virtual x86" + (DEBUG ? " - debug" : ""); + document.title = text + " - v86" + (DEBUG ? " - debug" : ""); const description = document.querySelector("meta[name=description]"); description && (description.content = "Running " + text); } From d39705cef4b18490a77438a5e7b41aec4dd8910e Mon Sep 17 00:00:00 2001 From: Fabian Date: Mon, 7 Oct 2024 09:26:53 -0600 Subject: [PATCH 22/90] dead code --- src/cpu.js | 2 +- src/vga.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/cpu.js b/src/cpu.js index 0a79376d6a..4eb44d61d0 100644 --- a/src/cpu.js +++ b/src/cpu.js @@ -940,7 +940,7 @@ CPU.prototype.init = function(settings, device_bus) this.devices.dma = new DMA(this); - this.devices.vga = new VGAScreen(this, device_bus, settings.screen, settings.vga_memory_size || 8 * 1024 * 1024, settings.screen_options || {}); + this.devices.vga = new VGAScreen(this, device_bus, settings.screen, settings.vga_memory_size || 8 * 1024 * 1024); this.devices.ps2 = new PS2(this, device_bus); diff --git a/src/vga.js b/src/vga.js index 660f61c3c8..0d85da06a7 100644 --- a/src/vga.js +++ b/src/vga.js @@ -50,9 +50,8 @@ const VGA_HOST_MEMORY_SPACE_SIZE = Uint32Array.from([ * @param {BusConnector} bus * @param {ScreenAdapter|DummyScreenAdapter} screen * @param {number} vga_memory_size - * @param {Object} options */ -function VGAScreen(cpu, bus, screen, vga_memory_size, options) +function VGAScreen(cpu, bus, screen, vga_memory_size) { this.cpu = cpu; From f5a9fda7b86b5d0a0b14ca8b9877dd8314c5f138 Mon Sep 17 00:00:00 2001 From: Fabian Date: Fri, 11 Oct 2024 18:45:26 -0600 Subject: [PATCH 23/90] add little kernel suggested by @SuperMaxusa --- src/browser/main.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/browser/main.js b/src/browser/main.js index 985ca24a2f..642da2bdbb 100644 --- a/src/browser/main.js +++ b/src/browser/main.js @@ -890,6 +890,16 @@ name: "Tilck", homepage: "https://github.com/vvaltchev/tilck", }, + { + id: "littlekernel", + multiboot: { + url: host + "littlekernel-multiboot.img", + async: false, + size: 969580, + }, + name: "Little Kernel", + homepage: "https://github.com/littlekernel/lk", + }, { id: "sanos", memory_size: 128 * 1024 * 1024, From 282a2a275d3a47055e8c233df56d9b29985a54d8 Mon Sep 17 00:00:00 2001 From: Fabian Date: Fri, 11 Oct 2024 18:49:50 -0600 Subject: [PATCH 24/90] minor changes --- src/browser/main.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/browser/main.js b/src/browser/main.js index 642da2bdbb..cf408e6155 100644 --- a/src/browser/main.js +++ b/src/browser/main.js @@ -304,7 +304,7 @@ fixed_chunk_size: 256 * 1024, use_parts: true, }, - name: "MS-DOS", + name: "MS-DOS 6.22", }, { id: "msdos4", @@ -312,7 +312,7 @@ url: host + "msdos4.img", size: 1474560, }, - name: "MS-DOS", + name: "MS-DOS 4", }, { id: "freedos", @@ -378,7 +378,7 @@ url: host + "windows101.img", size: 1474560, }, - name: "Windows", + name: "Windows 1.01", }, { id: "windows2", @@ -435,7 +435,7 @@ size: 10068480, async: false, }, - name: "Buildroot Linux", + name: "Buildroot Linux 6.8", filesystem: {}, cmdline: "tsc=reliable mitigations=off random.trust_cpu=on", }, @@ -1059,7 +1059,7 @@ fixed_chunk_size: 1024 * 1024, use_parts: true, }, - name: "Android", + name: "Android 4", }, { id: "tinycore", @@ -1812,7 +1812,7 @@ }, CLEAR_STATS ? 5000 : 1000); } - if(["dsl", "helenos", "android", "android4"].includes(profile?.id)) + if(["dsl", "helenos", "android", "android4", "redox"].includes(profile?.id)) { setTimeout(() => { // hack: Start automatically From 43af8187b8def16c7f3bb71396269ce151656a6b Mon Sep 17 00:00:00 2001 From: Fabian Date: Tue, 15 Oct 2024 11:16:37 -0600 Subject: [PATCH 25/90] add BeOS 5 suggested by @SuperMaxusa --- src/browser/main.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/browser/main.js b/src/browser/main.js index cf408e6155..8a0ae5aefc 100644 --- a/src/browser/main.js +++ b/src/browser/main.js @@ -295,6 +295,18 @@ name: "Haiku", homepage: "https://www.haiku-os.org/", }, + { + id: "beos", + memory_size: 512 * 1024 * 1024, + hda: { + url: host + "beos5/.img", + size: 536870912, + async: true, + fixed_chunk_size: 1024 * 1024, + use_parts: true, + }, + name: "BeOS 5", + }, { id: "msdos", hda: { @@ -1812,7 +1824,7 @@ }, CLEAR_STATS ? 5000 : 1000); } - if(["dsl", "helenos", "android", "android4", "redox"].includes(profile?.id)) + if(["dsl", "helenos", "android", "android4", "redox", "beos"].includes(profile?.id)) { setTimeout(() => { // hack: Start automatically From 0d4a30485a907078c7a035d04c5edb0bfae57359 Mon Sep 17 00:00:00 2001 From: Fabian Date: Tue, 15 Oct 2024 11:17:00 -0600 Subject: [PATCH 26/90] allow ctrl+click --- src/browser/main.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/browser/main.js b/src/browser/main.js index 8a0ae5aefc..7173415e94 100644 --- a/src/browser/main.js +++ b/src/browser/main.js @@ -1390,9 +1390,12 @@ { element.onclick = e => { - e.preventDefault(); - element.blur(); - start_emulation(os, null); + if(!e.ctrlKey) + { + e.preventDefault(); + element.blur(); + start_emulation(os, null); + } }; } } From 0ae824852182528a279d080a45c0ade25cdbb61a Mon Sep 17 00:00:00 2001 From: Fabian Date: Tue, 15 Oct 2024 11:17:22 -0600 Subject: [PATCH 27/90] format url parameters nicer --- src/browser/main.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/browser/main.js b/src/browser/main.js index 7173415e94..2373ba5355 100644 --- a/src/browser/main.js +++ b/src/browser/main.js @@ -1539,7 +1539,8 @@ { $("boot_options").style.display = "none"; - const new_query_args = new URLSearchParams({ "profile": profile?.id || "custom" }); + const new_query_args = new Map(); + new_query_args.set("profile", profile?.id || "custom"); const settings = {}; @@ -1652,10 +1653,10 @@ if(!settings.relay_url) { settings.relay_url = $("relay_url").value; - if(!DEFAULT_NETWORKING_PROXIES.includes(settings.relay_url)) new_query_args.append("relay_url", settings.relay_url); + if(!DEFAULT_NETWORKING_PROXIES.includes(settings.relay_url)) new_query_args.set("relay_url", settings.relay_url); } settings.disable_audio = $("disable_audio").checked || settings.disable_audio; - if(settings.disable_audio) new_query_args.append("mute", "1"); + if(settings.disable_audio) new_query_args.set("mute", "1"); // some settings cannot be overridden when a state image is used if(!settings.initial_state) @@ -1714,26 +1715,26 @@ { settings.memory_size = memory_size * MB; } - if(memory_size !== DEFAULT_MEMORY_SIZE) new_query_args.append("m", String(memory_size)); + if(memory_size !== DEFAULT_MEMORY_SIZE) new_query_args.set("m", String(memory_size)); const vga_memory_size = parseInt($("vga_memory_size").value, 10) || DEFAULT_VGA_MEMORY_SIZE; if(!settings.vga_memory_size || vga_memory_size !== DEFAULT_VGA_MEMORY_SIZE) { settings.vga_memory_size = vga_memory_size * MB; } - if(vga_memory_size !== DEFAULT_VGA_MEMORY_SIZE) new_query_args.append("vram", String(vga_memory_size)); + if(vga_memory_size !== DEFAULT_VGA_MEMORY_SIZE) new_query_args.set("vram", String(vga_memory_size)); const boot_order = parseInt($("boot_order").value, 16) || DEFAULT_BOOT_ORDER; if(!settings.boot_order || boot_order !== DEFAULT_BOOT_ORDER) { settings.boot_order = boot_order; } - if(settings.boot_order !== DEFAULT_BOOT_ORDER) new_query_args.append("boot_order", String(settings.boot_order)); + if(settings.boot_order !== DEFAULT_BOOT_ORDER) new_query_args.set("boot_order", String(settings.boot_order)); if(settings.acpi === undefined) { settings.acpi = $("acpi").checked; - if(settings.acpi) new_query_args.append("acpi", "1"); + if(settings.acpi) new_query_args.set("acpi", "1"); } if(!settings.bios) @@ -2541,7 +2542,7 @@ { if(window.history.pushState) { - const search = "?" + params.toString(); + let search = "?" + params.entries().map(([key, value]) => key + "=" + value.replace(/[?&=#+]/g, encodeURIComponent))["toArray"]().join("&"); window.history.pushState({ search }, "", search); } } From e11feac92d4fae1dd98ae00673c0e40865bd5c11 Mon Sep 17 00:00:00 2001 From: Fabian Date: Tue, 15 Oct 2024 11:29:30 -0600 Subject: [PATCH 28/90] resume audio context when clicking on screen (fix #1121) --- src/browser/main.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/browser/main.js b/src/browser/main.js index 2373ba5355..728e83dbfc 100644 --- a/src/browser/main.js +++ b/src/browser/main.js @@ -2338,6 +2338,11 @@ $("screen_container").onclick = function() { + if(emulator.is_running() && emulator.speaker_adapter && emulator.speaker_adapter.audio_context.state === "suspended") + { + emulator.speaker_adapter.audio_context.resume(); + } + if(mouse_is_enabled && os_uses_mouse) { emulator.lock_mouse(); From cda79d9ebc3bd63ad773e476eac0e8c55dc7c996 Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 16 Oct 2024 12:43:38 -0600 Subject: [PATCH 29/90] fix: don't overwrite acpi setting --- src/browser/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/main.js b/src/browser/main.js index 728e83dbfc..967bf9083d 100644 --- a/src/browser/main.js +++ b/src/browser/main.js @@ -1640,7 +1640,7 @@ settings.vga_memory_size = vram * 1024 * 1024; } - settings.acpi = query_args.has("acpi") ? bool_arg(query_args.get("acpi")) : undefined; + settings.acpi = query_args.has("acpi") ? bool_arg(query_args.get("acpi")) : settings.acpi; settings.use_bochs_bios = query_args.get("bios") === "bochs"; settings.net_device_type = query_args.get("net_device_type") === "virtio" ? "virtio" : "ne2k"; } From e44a2b02625c98a46a552d57ae61b7de904b0fbc Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 16 Oct 2024 12:53:49 -0600 Subject: [PATCH 30/90] update ReactOS image provided by @SuperMaxusa --- src/browser/main.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/browser/main.js b/src/browser/main.js index 967bf9083d..753a2d99bd 100644 --- a/src/browser/main.js +++ b/src/browser/main.js @@ -952,28 +952,30 @@ id: "reactos", memory_size: 512 * 1024 * 1024, hda: { - url: host + "reactos/.img", - size: 500 * 1024 * 1024, + url: host + "reactos-v2/.img", + size: 681574400, async: true, fixed_chunk_size: 1024 * 1024, use_parts: true, }, - state: { url: host + "reactos_state.bin.zst" }, + state: { url: host + "reactos_state-v2.bin.zst" }, mac_address_translation: true, name: "ReactOS", + acpi: true, homepage: "https://reactos.org/", }, { id: "reactos-boot", memory_size: 512 * 1024 * 1024, hda: { - url: host + "reactos/.img", - size: 500 * 1024 * 1024, + url: host + "reactos-v2/.img", + size: 681574400, async: true, fixed_chunk_size: 1024 * 1024, use_parts: true, }, name: "ReactOS", + acpi: true, homepage: "https://reactos.org/", }, { From fcec4ee68914ab9b77b63df3d40d1be304bdcd63 Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 16 Oct 2024 14:16:34 -0600 Subject: [PATCH 31/90] minor: update some os names/urls --- src/browser/main.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/browser/main.js b/src/browser/main.js index 753a2d99bd..a268e61b75 100644 --- a/src/browser/main.js +++ b/src/browser/main.js @@ -478,6 +478,7 @@ async: false, }, name: "ELKS", + homepage: "https://github.com/ghaerr/elks", }, { id: "nodeos", @@ -526,7 +527,7 @@ }, { id: "unix-v7", - name: "Unix V7/x86", + name: "Unix V7", hda: { url: host + "unix-v7x86-0.8a/.img", size: 152764416, @@ -920,7 +921,7 @@ async: false, size: 1474560, }, - name: "sanos", + name: "Sanos", homepage: "http://www.jbox.dk/sanos/", }, { From bccf45b3da32a7b8402f2e0ade72085e157d2e2c Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 16 Oct 2024 14:17:09 -0600 Subject: [PATCH 32/90] proxy url presets --- index.html | 3 ++- src/browser/main.js | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index cce30e2178..9c50016c2a 100644 --- a/index.html +++ b/index.html @@ -126,7 +126,8 @@

Setup

- +
+ Presets: none, public relay, wisp, fetch diff --git a/src/browser/main.js b/src/browser/main.js index a268e61b75..d5109421e1 100644 --- a/src/browser/main.js +++ b/src/browser/main.js @@ -1454,6 +1454,19 @@ start_emulation(profile, query_args); }); } + + function set_proxy_value(id, value) + { + const elem = $(id); + if(elem) + { + elem.onclick = () => $("relay_url").value = value; + } + } + set_proxy_value("network_none", ""); + set_proxy_value("network_fetch", "fetch"); + set_proxy_value("network_relay", "wss://relay.widgetry.org/"); + set_proxy_value("network_wisp", "wisps://wisp.mercurywork.shop/v86/"); } function debug_onload() From 4d8b78d0fa027afea834b6a564c2d112f6e819ee Mon Sep 17 00:00:00 2001 From: Fabian Date: Fri, 18 Oct 2024 16:03:48 -0600 Subject: [PATCH 33/90] new layout for frontpage, filters, mobile-friendly table --- index.html | 157 +++++++++++++++++++++++++++++--------------- src/browser/main.js | 66 +++++++++++++++++++ v86.css | 85 ++++++++++++++++++++---- 3 files changed, 243 insertions(+), 65 deletions(-) diff --git a/index.html b/index.html index 9c50016c2a..e3e540d8ae 100644 --- a/index.html +++ b/index.html @@ -10,64 +10,113 @@
-

Select profile

+
+ Family: + + + + + + + + UI: + + + Medium: + + + + Size: + + + + Status: + + + License: + + + Arch: + + + + + + Lang: + + + + + + +
+
+
+ - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Arch Linux 12 MB >_ - Complete Arch Linux with various compilers, networking and Xorg. Restored from snapshot.
Damn Small Linux 50 MB 💻 - Graphical Linux with 2.4 kernel, Firefox 2.0 and more. Takes 1 minute to boot.
Buildroot Linux 5.0 MB >_ - Minimal Linux with busybox, Lua, tests, internet access, ping, telnet and curl. Exchange files through /mnt/.
ReactOS 18 MB 💻 - Windows-compatible OS with QtWeb and Breakout. Restored from snapshot.
Windows 2000 22 MB 💻 - Including Pinball and Internet Explorer with internet access. Additional sectors are loaded as needed.
Windows 98 9.7 MB 💻 - Including Minesweeper and Internet Explorer with internet access. Additional sectors are loaded as needed.
Windows 95 4.6 MB 💻 - Restored from snapshot
Windows 3.1 15 MB 💻 - Takes 15 seconds to boot
Windows 1.01 0.6 MB 💻 - The first version of Microsoft Windows
MS-DOS 6.22 4.4 MB >_ - With Enhanced Tools, QBasic, vim, games and demos.
FreeDOS 0.5 MB >_ - With nasm, vim, debug.com, Rogue, some games and demos.
FreeBSD 17 MB >_ - FreeBSD 12.0 base install. Restored from snapshot.
OpenBSD 12 MB >_ - OpenBSD 6.6 base install. Restored from snapshot.
9front 4.4 MB 💻 - A Plan 9 fork.
Haiku 38 MB 💻 - An open-source operating system inspired by BeOS. Restored from snapshot. Includes network support.
SerenityOS 17 MB 💻 - A graphical Unix-like operating system. Restored from snapshot.
HelenOS 7.9 MB 💻 - A graphical operating system based on a multiserver microkernel design
FiwixOS 15 MB >_ - A Unix-like OS written from scratch. Includes Doom.
Android-x86 42 MB 💻 - An x86 port of the Android 1.6. Quite slow. Takes about 3 minutes to boot.
Oberon 1.2 MB 💻 - Native Oberon 2.3.6
KolibriOS 1.4 MB 💻 - Fast graphical OS written in Assembly
QNX 1.3 MB 💻 - QNX 4.05 Demo disk (no networking)
Snowdrop 0.3 MB >_ - A homebrew operating system from scratch, written in assembly language
Solar OS 0.3 MB 💻 - Simple graphical OS
Bootchess 512 B >_ - A tiny chess program written in the boot sector
SectorLISP 512 B >_ - A LISP interpreter that fits into the boot sector
NameSizeUIFamilyArchStatusSourceLangMediumNotes
Arch Linux 15+ MB Linux 32-bit Modern Open-source C 9pfs Xorg, Firefox, various compilers and more
Damn Small Linux 50 MB Linux 32-bit Historic Open-source C CD 4.11.rc2 with Firefox 2.0
Buildroot Linux 4.9 MB Linux 32-bit Modern Open-source C bzImage Lua, ping, curl, telnet
FreeBSD 16+ MB BSD 32-bit Modern Open-source C HD FreeBSD 12.0
OpenBSD 11+ MB BSD 32-bit Modern Open-source C HD OpenBSD 6.6
FiwixOS 15+ MB Unix-like 32-bit Modern Open-source C HD With Doom
SerenityOS 16+ MB Unix-like 32-bit Modern Open-source C++ HD Web browser, various games and demos
Haiku 41+ MB BeOS 32-bit Modern Open-source C++ HD Networking (WebPositive), OCaml, 2048, NetHack
Tiny Aros 17+ MB AmigaOS 32-bit Modern Open-source C CD AmigaOS-like graphical OS
ReactOS 17+ MB Windows-like 32-bit Modern Open-source C++ HD QtWeb, LBreakout2, OpenTTD, Bochs, TCC
Windows 1.01 0.7 MB Windows 16-bit Historic Proprietary ASM, C Floppy Reversi, Paint
Windows 95 19+ MB Windows 32-bit Historic Proprietary ASM, C HD Age of Empires, FASM, POV-Ray, Hover!
Windows 2000 23+ MB Windows 32-bit Historic Proprietary C++ HD IE 5, Pinball
MS-DOS 6.22 2.4+ MB DOS 16-bit Historic Proprietary ASM HD Doom, Sim City, OCaml 1.0, Turbo C and more
FreeDOS 0.6 MB DOS 16-bit Modern Open-source ASM, C Floppy nasm, vim, debug.com, Rogue, various demos
KolibriOS 1.3 MB Custom 32-bit Modern Open-source ASM Floppy Various apps, games and demos
QNX 4.05 1.4 MB Custom 32-bit Historic Proprietary C Floppy 1999 demo disk

-

Setup

+

Setup

diff --git a/src/browser/main.js b/src/browser/main.js index d5109421e1..e9f469e103 100644 --- a/src/browser/main.js +++ b/src/browser/main.js @@ -1455,6 +1455,72 @@ }); } + const os_info = Array.from(document.querySelectorAll("#oses tbody tr")).map(element => + { + const [_, size_raw, unit] = element.children[1].textContent.match(/([\d\.]+)\+? (\w+)/); + let size = +size_raw; + if(unit === "MB") size *= 1024 * 1024; + else if(unit === "KB") size *= 1024; + return { + element, + size, + graphical: element.children[2].firstChild.className === "gui_icon", + family: element.children[3].textContent.replace(/-like/, ""), + arch: element.children[4].textContent, + status: element.children[5].textContent, + source: element.children[6].textContent, + languages: new Set(element.children[7].textContent.split(", ")), + medium: element.children[8].textContent, + }; + }); + + const filter_elements = document.querySelectorAll("#filter input"); + for(const element of filter_elements) + { + element.onchange = update_filters; + } + + function update_filters() + { + const filter = {}; + for(const element of filter_elements) + { + filter[element.id.replace(/filter_/, "")] = element.checked; + } + + const show_all = !Object.values(filter).includes(true); + for(const os of os_info) + { + const show = show_all || + filter["graphical"] && os.graphical || + filter["text"] && !os.graphical || + filter["linux"] && os.family === "Linux" || + filter["bsd"] && os.family === "BSD" || + filter["windows"] && os.family === "Windows" || + filter["unix"] && os.family === "Unix" || + filter["dos"] && os.family === "DOS" || + filter["custom"] && os.family === "Custom" || + filter["floppy"] && os.medium === "Floppy" || + filter["cd"] && os.medium === "CD" || + filter["hd"] && os.medium === "HD" || + filter["modern"] && os.status === "Modern" || + filter["historic"] && os.status === "Historic" || + filter["opensource"] && os.source === "Open-source" || + filter["proprietary"] && os.source === "Proprietary" || + filter["bootsector"] && os.size <= 512 || + filter["lt5mb"] && os.size <= 5 * 1024 * 1024 || + filter["gt5mb"] && os.size > 5 * 1024 * 1024 || + filter["16bit"] && os.arch === "16-bit" || + filter["32bit"] && os.arch === "32-bit" || + filter["asm"] && os.languages.has("ASM") || + filter["c"] && os.languages.has("C") || + filter["cpp"] && os.languages.has("C++") || + filter["other_lang"] && ["Java", "Haskell", "Rust", "Erlang", "Oberon"].some(l => os.languages.has(l)); + + os.element.style.display = show ? "" : "none"; + } + } + function set_proxy_value(id, value) { const elem = $(id); diff --git a/v86.css b/v86.css index d25f480c18..5088ef52e3 100644 --- a/v86.css +++ b/v86.css @@ -63,6 +63,7 @@ body { a { color: wheat; text-decoration: none; + cursor: pointer; } .phone_keyboard { width: 0; @@ -106,27 +107,80 @@ h4 { font-weight: bold; font-size: 16px; } -#boot_options td { +#boot_options td, #boot_options th { padding: 1px 7px; } -#oses small { - font-size: 80%; - color: #ccc; - padding-left: 5px; -} -#oses tr { + +#oses tbody tr { cursor: pointer; } #oses { border-spacing: 0; } -#oses tr:hover { +#oses tbody tr:hover { background-color: #311; } -#oses td:nth-child(1) { - white-space: pre; - vertical-align: top; +#oses td:nth-child(2) { + text-align: right; +} +#oses thead { + text-align: left; +} + +/* This is the best I managed to do with my little css experience. + If you can do better, please send a PR. */ +@media (max-width: 1250px) { + #oses td:nth-child(9), #oses th:nth-child(9) { + display: none; + } } +@media (max-width: 1150px) { + #oses td:nth-child(8), #oses td:nth-child(7), #oses th:nth-child(8), #oses th:nth-child(7) { + display: none; + } +} +@media (max-width: 1050px) { + #oses td:nth-child(5), #oses td:nth-child(6), #oses th:nth-child(5), #oses th:nth-child(6) { + display: none; + } +} +@media (max-width: 850px) { + #oses td:nth-child(4), #oses th:nth-child(4) { + display: none; + } +} +@media (max-width: 750px) { + #oses th:nth-child(2), #oses th:nth-child(3), #oses th:nth-child(10) { + display: none; + } + #oses td:nth-child(1), #oses td:nth-child(2), #oses td:nth-child(3) { + display: inline; + } + #oses td:nth-child(2), td:nth-child(3) { + font-size: smaller; + } + #oses td:nth-child(10) { + display: block; + padding-bottom: 10px; + } +} + +label { + user-select: none; + white-space: nowrap; +} +input[type=checkbox] { + vertical-align: middle; + position: relative; + bottom: 1px; + margin: 0; +} +#filter label { + background-color: #444; + padding: 2px 4px; + border-radius: 4px; +} + #terminal { max-width: 1024px; } @@ -147,6 +201,15 @@ h4 { } } +.gui_icon { + height: 1em; + content: url('data:image/svg+xml,'); +} +.tui_icon { + height: 1em; + content: url('data:image/svg+xml,'); +} + /* the code below was copied from xterm.css */ .xterm { From ba7f9405840b6ae476bf01a5b4dfb7f1a6e48055 Mon Sep 17 00:00:00 2001 From: Fabian Date: Sun, 20 Oct 2024 13:53:57 -0600 Subject: [PATCH 34/90] disable floppy insert/eject test for now --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index b6ee96ae5c..9857eedeb2 100644 --- a/Makefile +++ b/Makefile @@ -320,7 +320,7 @@ api-tests: all-debug ./tests/api/clean-shutdown.js ./tests/api/state.js ./tests/api/reset.js - ./tests/api/floppy-insert-eject.js + #./tests/api/floppy-insert-eject.js # disabled for now, sometimes hangs ./tests/api/serial.js ./tests/api/reboot.js From 0bf68c7f518308ac06a79e93d122958a847f1d53 Mon Sep 17 00:00:00 2001 From: SuperMaxusa <41739128+SuperMaxusa@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:43:29 +0200 Subject: [PATCH 35/90] docs: adding instructions for Windows 9x --- docs/windows-9x.md | 106 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 docs/windows-9x.md diff --git a/docs/windows-9x.md b/docs/windows-9x.md new file mode 100644 index 0000000000..1174070e1f --- /dev/null +++ b/docs/windows-9x.md @@ -0,0 +1,106 @@ +## Installing using QEMU + +Recommended versions: + - Windows 95 OSR2(.5) + - Windows 98 Second Edition (SE) + +------------- + +1. Create a disk image (up to 2 GB): +```sh +qemu-img -f raw hdd.img M +``` +2. Run QEMU with the following settings: +```sh +qemu-system-i386 -m 128 -M pc,acpi=off -hda hdd.img +``` + - add `-cdrom /path/to/installCD.iso`, if you use a CD version. + - add `-fda /path/to/boot_floppy.img -boot a`, if you use a floppy version or your install CD is non-bootable. + - (optionally) add `-device sb16` to enable sound + - (optionally) add `-nic user,model=ne2k_pci` or `-device ne2k_pci,netdev=<...>` to enable networking + +3. For Windows 98: select "Start Windows 98 Setup from CD-ROM". For Windows 95: select "Load NEC IDE CDROM driver" and run `fdisk` to create partition, restart emulator, run `format c:` and `D:\WIN95\SETUP`. + +4. To change floppy disk, press *Ctrl+Alt+2* to switch to the QEMU Monitor, run `change floppy0 /path/to/new_floppy_image` and press *Ctrl+Alt+1* to switch to VGA. +5. Follow the installation guide on the screen. +6. (optionally) If "Windows protection" errors appears on startup, apply [FIX95CPU](http://lonecrusader.x10host.com/fix95cpu.html) or [patcher9x](https://github.com/JHRobotics/patcher9x#installation). + +> [!TIP] +> For transfer files from host to guest, use [genisoimage](https://wiki.debian.org/genisoimage) ([UltraISO](https://www.ultraiso.com/) and [PowerISO](https://www.poweriso.com/) for Windows and Mac) for creating CD-ISO image or [dosfstools](https://github.com/dosfstools/dosfstools) ([WinImage](https://www.winimage.com/download.htm) for Windows) for creating floppy disk images, then mount the created image to QEMU. + +## Floppy disk support + +Currently, the floppy drive in v86 works only with MS-DOS compatibility mode. + +To check this: open the Start menu, click on "Control Panel" and "System", select "Performance" tab. +If it says *"Drive A is using MS-DOS compatibility mode file system"*, the floppy drive should work properly in v86. If not, try this solution: + +1. Click on "Device Manager" in "System Properties". +2. Open "Floppy disk controllers", select "Standard Floppy Disk Controller" and press "Remove" at the bottom. +3. Restart Windows. + +## Enabling True Color (32 bpp) + +The default VGA display driver only supports 640x480x8 video mode, to fix this, install **Universal VBE9x Video Display Driver**. + +> [!WARNING] +> After installing, DOS Mode (and other programs and games that require it) may not work properly. +> This is a problem in VBE9x, not v86, see [#110](https://github.com/copy/v86/issues/110). +> Also, this driver doesn't support DirectX, DirectDraw and OpenGL. + +1. Download driver from https://bearwindows.zcm.com.au/vbe9x.htm and unpack into Windows. +2. Right-click on the Desktop, click on "Properties". +3. Click "Advanced" > "Adapter" > "Change". +4. Press "Next", select "Display a of all the drivers in a specific location..." and press again "Next". +5. Press "Have Disk...", click "Browse" and go to folder with unpacked driver. Inside the folder with driver, should be folders like `032mb`, `064mb`, `128mb`. Choose a version based on needed video memory size (for example, `032mb`), then select `vbemp.inf` inside. +6. Select "VBE Miniport" adapter, press "OK" and "Next". +7. After installing, restart Windows. + +## CPU idling on Windows 95 +See about [installing AmnHLT](cpu-idling.md#windows-9x-using-amnhlt). + +## Enabling networking on Windows 95 (requires install CD) + +1. Open the Start menu, click on "Control Panel" and "Add New Hardware". +2. Press "Next", select "No" and select next options: + +``` +Hardware type: Network adapters +Manufacturers: Novell +Models: NE2000 Compatible +``` + +3. Press "Next" and restart Windows. +4. After restarting, right-click on "My computer", select "Propeties". +5. Open "Device Manager" tab, select "NE2000 Compatible" (in "Network adapters") and press "Properties" +6. Open "Resources", change values by selecting the properties and click on "Change Setting": + +``` +Interrupt Request: 10 +Input/Output Range: 0300 - 031F +``` + +7. In "Control Panel", open "Network", click on "Add", choose "Protocol" and select the following options: + +``` +Manufacturers: Microsoft +Network Protocols: TCP/IP +``` + +8. (optionally) Set "Primary Network Logon" to `Windows Logon`. + +## Enabling sound manually + +> [!NOTE] +> If you don't have an install CD, use the Sound Blaster 16 driver from https://www.claunia.com/qemu/drivers/index.html. + +1. Open "Start" menu, click on "Control Panel" and "Add New Hardware". +2. Press "Next", select "No" and select the following options: + +``` +Hardware type: Sound, video and game cotrollers +Manufacturers: Creative Labs +Models: Creative Labs Sound Blaster 16 or AWE-32 +``` + +3. Restart Windows. From 6719dfbfc5be14c749b0b0f3c6dc052a3cd2032d Mon Sep 17 00:00:00 2001 From: SuperMaxusa <41739128+SuperMaxusa@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:47:22 +0200 Subject: [PATCH 36/90] mention Windows 9x setup guide in readme.md --- Readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Readme.md b/Readme.md index 02eeaed549..b20eddbcb6 100644 --- a/Readme.md +++ b/Readme.md @@ -63,6 +63,7 @@ list of emulated hardware: [Alpine Linux guest setup](tools/docker/alpine/) — [Arch Linux guest setup](docs/archlinux.md) — [Windows 2000/XP guest setup](docs/windows-xp.md) — +[Windows 9x guest setup](docs/windows-9x.md) — [9p filesystem](docs/filesystem.md) — [Linux rootfs on 9p](docs/linux-9p-image.md) — [Profiling](docs/profiling.md) — @@ -94,6 +95,7 @@ Here's an overview of the operating systems supported in v86: - Windows 1, 3.x, 95, 98, ME, NT and 2000 work reasonably well. - In Windows 2000 and higher the PC type has to be changed from ACPI PC to Standard PC - There are some known boot issues ([#250](https://github.com/copy/v86/issues/250), [#433](https://github.com/copy/v86/issues/433), [#507](https://github.com/copy/v86/issues/507), [#555](https://github.com/copy/v86/issues/555), [#620](https://github.com/copy/v86/issues/620), [#645](https://github.com/copy/v86/issues/645)) + - See [Windows 9x guest setup](docs/windows-9x.md) - Windows XP, Vista and 8 work under certain conditions (see [#86](https://github.com/copy/v86/issues/86), [#208](https://github.com/copy/v86/issues/208)) - See [Windows 2000/XP guest setup](docs/windows-xp.md) - Many hobby operating systems work. From 15372291dbab4cfa141db26f028e0807780345cf Mon Sep 17 00:00:00 2001 From: SuperMaxusa <41739128+SuperMaxusa@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:54:04 +0200 Subject: [PATCH 37/90] mention Windows 9x setup guide in issue template --- .github/ISSUE_TEMPLATE/issue.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/issue.md b/.github/ISSUE_TEMPLATE/issue.md index 8e499465ce..0659918436 100644 --- a/.github/ISSUE_TEMPLATE/issue.md +++ b/.github/ISSUE_TEMPLATE/issue.md @@ -12,7 +12,7 @@ Welcome to v86's issue tracker! We use this tracker for bug reports or feature requests. For user support, questions or general comments, use the chat at https://gitter.im/copy/v86 or the forum at https://github.com/copy/v86/discussions -Please don't ask for support for any version of Windows. There are many existing issues at https://github.com/copy/v86/issues?q=is%253Aissue+windows. See also docs/windows-xp.md. +Please don't ask for support for any version of Windows. There are many existing issues at https://github.com/copy/v86/issues?q=is%253Aissue+windows. See also docs/windows-xp.md and docs/windows-9x.md. Before reporting OS incompatibilities, check existing issues and the compatibility section of the readme. From f767d4d73a7abe4300fa5dad1e68f15c250fc20b Mon Sep 17 00:00:00 2001 From: SuperMaxusa <41739128+SuperMaxusa@users.noreply.github.com> Date: Sun, 17 Nov 2024 22:29:45 +0200 Subject: [PATCH 38/90] fetch-networking: fix parsing of HTTP headers (#1182) --- src/browser/fetch_network.js | 48 ++++++++++++++++++++++++++++++---- tests/devices/fetch_network.js | 40 +++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/src/browser/fetch_network.js b/src/browser/fetch_network.js index 489938942c..2314a20724 100644 --- a/src/browser/fetch_network.js +++ b/src/browser/fetch_network.js @@ -83,11 +83,14 @@ async function on_data_http(data) let req_headers = new Headers(); for(let i = 1; i < headers.length; ++i) { - let parts = headers[i].split(": "); - let key = parts[0].toLowerCase(); - let value = parts[1]; - if( key === "host" ) target.host = value; - else if( key.length > 1 ) req_headers.set(parts[0], value); + const header = this.net.parse_http_header(headers[i]); + if(!header) { + console.warn('The request contains an invalid header: "%s"', headers[i]); + this.write(new TextEncoder().encode("HTTP/1.1 400 Bad Request\r\nContent-Length: 0")); + return; + } + if( header.key.toLowerCase() === "host" ) target.host = header.value; + else req_headers.append(header.key, header.value); } dbg_log("HTTP Dispatch: " + target.href, LOG_FETCH); @@ -151,6 +154,41 @@ FetchNetworkAdapter.prototype.fetch = async function(url, options) } }; +FetchNetworkAdapter.prototype.parse_http_header = function(header) +{ + const parts = header.match(/^([^:]*):(.*)$/); + if(!parts) { + dbg_log("Unable to parse HTTP header", LOG_FETCH); + return; + } + + const key = parts[1]; + const value = parts[2].trim(); + + if(key.length === 0) + { + dbg_log("Header key is empty, raw header", LOG_FETCH); + return; + } + if(value.length === 0) + { + dbg_log("Header value is empty", LOG_FETCH); + return; + } + if(!/^[\w-]+$/.test(key)) + { + dbg_log("Header key contains forbidden characters", LOG_FETCH); + return; + } + if(!/^[\x20-\x7E]+$/.test(value)) + { + dbg_log("Header value contains forbidden characters", LOG_FETCH); + return; + } + + return { key, value }; +}; + /** * @param {Uint8Array} data */ diff --git a/tests/devices/fetch_network.js b/tests/devices/fetch_network.js index 8f98bb78ca..244ec5e2a3 100755 --- a/tests/devices/fetch_network.js +++ b/tests/devices/fetch_network.js @@ -124,7 +124,45 @@ const tests = assert(/This domain is for use in illustrative examples in documents/.test(capture), "got example.org text"); }, }, - + { + name: "Forbidden character in header name", + start: () => + { + emulator.serial0_send("wget --header='test.v86: 123' -T 10 -O - test.domain\n"); + emulator.serial0_send("echo -e done\\\\tincorrect header name\n"); + }, + end_trigger: "done\tincorrect header name", + end: (capture) => + { + assert(/400 Bad Request/.test(capture), "got error 400"); + }, + }, + { + name: "Empty header value", + start: () => + { + emulator.serial0_send("wget --header='test:' -T 10 -O - test.domain\n"); + emulator.serial0_send("echo -e done\\\\tempty header value\n"); + }, + end_trigger: "done\tempty header value", + end: (capture) => + { + assert(/400 Bad Request/.test(capture), "got error 400"); + }, + }, + { + name: "Header without separator", + start: () => + { + emulator.serial0_send("wget --spider --header='testheader' -T 10 -O - test.domain\n"); + emulator.serial0_send("echo -e done\\\\theader without colon\n"); + }, + end_trigger: "done\theader without colon", + end: (capture) => + { + assert(/400 Bad Request/.test(capture), "got error 400"); + }, + } ]; const emulator = new V86({ From ea0754ac329262fb3071a99b8c9c450f4fca0d04 Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Mon, 4 Nov 2024 07:57:32 +0100 Subject: [PATCH 39/90] improve write() efficiency - new: class EthernetPacketEncoder, replaces all dynamically allocated write-buffers with a single, static buffer - new: class Uint8Stream, designed to replace TCPConnection.send_buffer[] with a dynamically growing ringbuffer - new: added internet checksum to synthesized UDP packets (required by mTCP) in function write_udp() - replaced internet checksum calculation with more efficient algorithm from RFC1071 (chapter 4.1) - increased TCP chunk size in TCPConnection.pump() from 500 to 1460 bytes (max. TCP payload size) - added function to efficiently copy an array into a DataView using TypedArray.set(), replaces a bunch of for-loops - added function to efficiently encode a string into a DataView using TextEncoder.encodeInto() - refactored all write_...()-functions to use EthernetPacketEncoder - added several named constants for ethernet/ipv4/udp/tcp-related offsets - removed several apparently unused "return true" statements --- src/browser/fake_network.js | 620 +++++++++++++++++++++--------------- 1 file changed, 369 insertions(+), 251 deletions(-) diff --git a/src/browser/fake_network.js b/src/browser/fake_network.js index 39ea9ba393..83a995b481 100644 --- a/src/browser/fake_network.js +++ b/src/browser/fake_network.js @@ -5,7 +5,6 @@ const ETHERTYPE_IPV4 = 0x0800; const ETHERTYPE_ARP = 0x0806; const ETHERTYPE_IPV6 = 0x86DD; - const IPV4_PROTO_ICMP = 1; const IPV4_PROTO_TCP = 6; const IPV4_PROTO_UDP = 17; @@ -18,17 +17,33 @@ const TWO_TO_32 = Math.pow(2, 32); const DHCP_MAGIC_COOKIE = 0x63825363; const V86_ASCII = [118, 56, 54]; -const TCP_STATE_CLOSED = "closed"; -const TCP_STATE_LISTEN = "listen"; -const TCP_STATE_ESTABLISHED = "established"; -const TCP_STATE_FIN_WAIT_1 = "fin-wait-1"; -const TCP_STATE_CLOSE_WAIT = "close-wait"; -const TCP_STATE_FIN_WAIT_2 = "fin-wait-2"; -const TCP_STATE_LAST_ACK = "last-ack"; -const TCP_STATE_CLOSING = "closing"; -const TCP_STATE_TIME_WAIT = "time-wait"; -const TCP_STATE_SYN_RECEIVED = "syn-received"; -const TCP_STATE_SYN_SENT = "syn-sent"; +//const TCP_STATE_CLOSED = "closed"; // unused (terminal state) +//const TCP_STATE_LISTEN = "listen"; // unused +const TCP_STATE_ESTABLISHED = "established"; // TCPConnection.process() +const TCP_STATE_FIN_WAIT_1 = "fin-wait-1"; // close() (graceful-shutdown initiator state) +//const TCP_STATE_CLOSE_WAIT = "close-wait"; // unused (graceful-shutdown receiver state) +//const TCP_STATE_FIN_WAIT_2 = "fin-wait-2"; // unused (graceful-shutdown initiator state) +//const TCP_STATE_LAST_ACK = "last-ack"; // unused (graceful-shutdown receiver state) +//const TCP_STATE_CLOSING = "closing"; // unused +//const TCP_STATE_TIME_WAIT = "time-wait"; // unused (graceful-shutdown initiator state) +const TCP_STATE_SYN_RECEIVED = "syn-received"; // WispNetworkAdapter.send() (wisp_network.js) +const TCP_STATE_SYN_SENT = "syn-sent"; // connect() + process() + +const ETH_HEADER_SIZE = 14; +const ETH_PAYLOAD_OFFSET = ETH_HEADER_SIZE; +const ETH_PAYLOAD_SIZE = 1500; +const ETH_TRAILER_SIZE = 4; +const ETH_FRAME_SIZE = ETH_HEADER_SIZE + ETH_PAYLOAD_SIZE + ETH_TRAILER_SIZE; +const IPV4_HEADER_SIZE = 20; +const IPV4_PAYLOAD_OFFSET = ETH_PAYLOAD_OFFSET + IPV4_HEADER_SIZE; +const IPV4_PAYLOAD_SIZE = ETH_PAYLOAD_SIZE - IPV4_HEADER_SIZE; +const UDP_HEADER_SIZE = 8; +const UDP_PAYLOAD_OFFSET = IPV4_PAYLOAD_OFFSET + UDP_HEADER_SIZE; +const UDP_PAYLOAD_SIZE = IPV4_PAYLOAD_SIZE - UDP_HEADER_SIZE; +const TCP_HEADER_SIZE = 20; +const TCP_PAYLOAD_OFFSET = IPV4_PAYLOAD_OFFSET + TCP_HEADER_SIZE; +const TCP_PAYLOAD_SIZE = IPV4_PAYLOAD_SIZE - TCP_HEADER_SIZE; +const ICMP_HEADER_SIZE = 4; function a2ethaddr(bytes) { return [0,1,2,3,4,5].map((i) => bytes[i].toString(16)).map(x => x.length === 1 ? "0" + x : x).join(":"); @@ -43,6 +58,220 @@ function iptolong(parts) { return parts[0] << 24 | parts[1] << 16 | parts[2] << 8 | parts[3]; } +class Uint8Stream +{ + /** + * @param {number} initial_capacity + * @param {number} maximum_capacity + */ + constructor(initial_capacity, maximum_capacity) + { + this.maximum_capacity = maximum_capacity; + this.tail = 0; + this.head = 0; + this.length = 0; + this.buffer = new Uint8Array(initial_capacity); + } + + /** + * @param {number} capacity + */ + resize(capacity) + { + const new_buffer = new Uint8Array(capacity); + this.peek(new_buffer, this.length); + this.tail = 0; + this.head = this.length; + this.buffer = new_buffer; + } + + /** + * @param {Uint8Array} src_array + */ + write(src_array) + { + const src_length = src_array.length; + const total_length = this.length + src_length; + let capacity = this.buffer.length; + if (capacity < total_length) { + while (capacity < total_length) { + capacity *= 2; + } + if (this.maximum_capacity && capacity > this.maximum_capacity) { + console.error('stream capacity overflow in Uint8Stream.write()'); + return; + } + this.resize(capacity); + } + const buffer = this.buffer; + + /* + let head = this.head; + for (let i=0; i capacity) { + const i_split = capacity - this.head; + buffer.set(src_array.subarray(0, i_split), this.head); + buffer.set(src_array.subarray(i_split)); + } + else { + buffer.set(src_array, this.head); + } + this.head = new_head % capacity; + this.length += src_length; + } + + /** + * @param {Uint8Array} dst_array + * @param {number} length + */ + peek(dst_array, length) + { + if (length > this.length) { + length = this.length; + } + if (length) { + const buffer = this.buffer; + const capacity = buffer.length; + /* + for (let i=0, tail = this.tail; i capacity) { + const buf_len_left = new_tail % capacity; + const buf_len_right = capacity - this.tail; + dst_array.set(buffer.subarray(this.tail)); + dst_array.set(buffer.subarray(0, buf_len_left), buf_len_right); + } + else { + dst_array.set(buffer.subarray(this.tail, new_tail)); + } + } + return length; + } + + /** + * @param {number} length + */ + remove(length) + { + if (length > this.length) { + length = this.length; + } + if (length) { + this.tail = (this.tail + length) % this.buffer.length; + this.length -= length; + } + return length; + } +} + +class EthernetPacketEncoder +{ + constructor() + { + const eth_frame = new Uint8Array(ETH_FRAME_SIZE); + const offset = eth_frame.byteOffset; + + this.eth_frame = eth_frame; + this.eth_frame_view = new DataView(eth_frame.buffer); + this.eth_payload_view = new DataView(eth_frame.buffer, offset + ETH_PAYLOAD_OFFSET, ETH_PAYLOAD_SIZE); + this.ipv4_payload_view = new DataView(eth_frame.buffer, offset + IPV4_PAYLOAD_OFFSET, IPV4_PAYLOAD_SIZE); + this.udp_payload_view = new DataView(eth_frame.buffer, offset + UDP_PAYLOAD_OFFSET, UDP_PAYLOAD_SIZE); + + for (const view of [this.eth_frame_view, this.eth_payload_view, this.ipv4_payload_view, this.udp_payload_view]) { + view.setArray = this.view_setArray.bind(this, view); + view.setString = this.view_setString.bind(this, view); + view.setInetChecksum = this.view_setInetChecksum.bind(this, view); + } + + this.text_encoder = new TextEncoder(); + } + + /** + * Copy given data array into dst_view[dst_offset], return number of bytes written. + * + * @param {DataView} dst_view + * @param {number} dst_offset + * @param data + */ + view_setArray(dst_view, dst_offset, data) + { + this.eth_frame.set(data, dst_view.byteOffset + dst_offset); + return data.length; + } + + /** + * UTF8-encode given string into dst_view[dst_offset], return number of bytes written. + * + * @param {DataView} dst_view + * @param {number} dst_offset + * @param {string} str + */ + view_setString(dst_view, dst_offset, str) + { + const ofs = dst_view.byteOffset + dst_offset; + const result = this.text_encoder.encodeInto(str, ofs ? this.eth_frame.subarray(ofs) : this.eth_frame); + return result.written; + } + + /** + * Calculate 16-bit internet checksum for dst_view[0 : length], store result in dst_view[dst_offset]. + * Source: RFC768 and RFC1071 (chapter 4.1). + * + * @param {DataView} dst_view + * @param {number} dst_offset + * @param {number} length + * @param {number} checksum + */ + view_setInetChecksum(dst_view, dst_offset, length, checksum) + { + const data = this.eth_frame; + const offset = dst_view.byteOffset; + const uint16_end = offset + (length & ~1); + for (let i = offset; i < uint16_end; i += 2) { + checksum += data[i] << 8 | data[i+1]; + } + if (length & 1) { + checksum += data[uint16_end] << 8; + } + while (checksum >> 16) { + checksum = (checksum & 0xffff) + (checksum >> 16); + } + dst_view.setUint16(dst_offset, ~checksum); + } + + /** + * Encode and return ethernet packet for given spec. + * TODO: what about the trailing 32-bit ethernet checksum? + * + * @param {Object} spec + */ + encode_packet(spec) + { + dbg_assert(spec.eth); + this.eth_frame.fill(0); + const length = write_eth(spec, this.eth_frame_view, this); + return this.eth_frame.subarray(0, length); + } +} + +const ethernet_encoder = new EthernetPacketEncoder(); + +function make_packet(spec) +{ + return ethernet_encoder.encode_packet(spec); +} + function handle_fake_tcp(packet, adapter) { let reply = {}; @@ -52,24 +281,19 @@ function handle_fake_tcp(packet, adapter) src: packet.ipv4.dest, dest: packet.ipv4.src }; - - let tuple = [ - packet.ipv4.src.join("."), - packet.tcp.sport, - packet.ipv4.dest.join("."), - packet.tcp.dport - ].join(":"); - + const tuple = `${packet.ipv4.src.join(".")}:${packet.tcp.sport}:${packet.ipv4.dest.join(".")}:${packet.tcp.dport}`; if(packet.tcp.syn) { - if(adapter.tcp_conn[tuple]) { + if(adapter.tcp_conn.hasOwnProperty(tuple)) { dbg_log("SYN to already opened port", LOG_FETCH); } - if(adapter.on_tcp_connection(adapter, packet, tuple)) return; + if(adapter.on_tcp_connection(adapter, packet, tuple)) { + return; + } } - if(!adapter.tcp_conn[tuple]) { - dbg_log(`I dont know about ${tuple}, so restting`, LOG_FETCH); + if(!adapter.tcp_conn.hasOwnProperty(tuple)) { + dbg_log(`I dont know about ${tuple}, so resetting`, LOG_FETCH); let bop = packet.tcp.ackn; if(packet.tcp.fin || packet.tcp.syn) bop += 1; reply.tcp = { @@ -214,14 +438,15 @@ function handle_fake_dhcp(packet, adapter) { reply.dhcp.options = options; adapter.receive(make_packet(reply)); - } function handle_fake_networking(data, adapter) { let packet = {}; parse_eth(data, packet); if(packet.tcp) { - if(handle_fake_tcp(packet, adapter)) return true; + if(handle_fake_tcp(packet, adapter)) { + return true; + } } if(packet.arp && packet.arp.oper === 1 && packet.arp.ptype === ETHERTYPE_IPV4) { @@ -229,11 +454,15 @@ function handle_fake_networking(data, adapter) { } if(packet.dns) { - if(handle_fake_dns(packet, adapter)) return; + if(handle_fake_dns(packet, adapter)) { + return; + } } if(packet.ntp) { - if(handle_fake_ntp(packet, adapter)) return; + if(handle_fake_ntp(packet, adapter)) { + return; + } } // ICMP Ping @@ -242,7 +471,9 @@ function handle_fake_networking(data, adapter) { } if(packet.dhcp) { - if(handle_fake_dhcp(packet, adapter)) return; + if(handle_fake_dhcp(packet, adapter)) { + return; + } } if(packet.udp && packet.udp.dport === 8) { @@ -265,7 +496,7 @@ function parse_eth(data, o) { o.eth = eth; // Remove CRC from the end of the packet maybe? - let payload = data.subarray(14, data.length); + let payload = data.subarray(ETH_HEADER_SIZE, data.length); if(ethertype === ETHERTYPE_IPV4) { parse_ipv4(payload, o); @@ -281,18 +512,16 @@ function parse_eth(data, o) { } } -function write_eth(spec, data) { - let view = new DataView(data.buffer, data.byteOffset, data.byteLength); +function write_eth(spec, view, eth_encoder) { + view.setArray(0, spec.eth.dest); + view.setArray(6, spec.eth.src); view.setUint16(12, spec.eth.ethertype); - for(let i = 0; i < 6; ++i ) view.setUint8(0 + i, spec.eth.dest[i]); - for(let i = 0; i < 6; ++i ) view.setUint8(6 + i, spec.eth.src[i]); - - let len = 14; + let len = ETH_HEADER_SIZE; if(spec.arp) { - len += write_arp(spec, data.subarray(14)); + len += write_arp(spec, eth_encoder.eth_payload_view); } - if(spec.ipv4) { - len += write_ipv4(spec, data.subarray(14)); + else if(spec.ipv4) { + len += write_ipv4(spec, eth_encoder.eth_payload_view, eth_encoder); } return len; } @@ -314,26 +543,17 @@ function parse_arp(data, o) { o.arp = arp; } -function write_arp(spec, data) { - let view = new DataView(data.buffer, data.byteOffset, data.byteLength); +function write_arp(spec, view) { view.setUint16(0, spec.arp.htype); view.setUint16(2, spec.arp.ptype); view.setUint8(4, spec.arp.sha.length); view.setUint8(5, spec.arp.spa.length); view.setUint16(6, spec.arp.oper); - - for(let i = 0; i < 6; ++i) { - view.setUint8(8 + i, spec.arp.sha[i]); - view.setUint8(18 + i, spec.arp.tha[i]); - } - - for(let i = 0; i < 4; ++i) { - view.setUint8(14 + i, spec.arp.spa[i]); - view.setUint8(24 + i, spec.arp.tpa[i]); - } - + view.setArray(8, spec.arp.sha); + view.setArray(14, spec.arp.spa); + view.setArray(18, spec.arp.tha); + view.setArray(24, spec.arp.tpa); return 28; - } function parse_ipv4(data, o) { @@ -369,39 +589,27 @@ function parse_ipv4(data, o) { if(proto === IPV4_PROTO_ICMP) { parse_icmp(ipdata, o); } - if(proto === IPV4_PROTO_TCP) { + else if(proto === IPV4_PROTO_TCP) { parse_tcp(ipdata, o); } - if(proto === IPV4_PROTO_UDP) { + else if(proto === IPV4_PROTO_UDP) { parse_udp(ipdata, o); } - - - return true; } -function write_ipv4(spec, data) { - let view = new DataView(data.buffer, data.byteOffset, data.byteLength); - - let ihl = 5; // 20 byte header length normally - let version = 4; - let len = 4 * ihl; // Total Length +function write_ipv4(spec, view, eth_encoder) { + const ihl = IPV4_HEADER_SIZE >> 2; // header length in 32-bit words + const version = 4; + let len = IPV4_HEADER_SIZE; if(spec.icmp) { - len += write_icmp(spec, data.subarray(ihl * 4)); + len += write_icmp(spec, eth_encoder.ipv4_payload_view); } - if(spec.udp) { - len += write_udp(spec, data.subarray(ihl * 4)); + else if(spec.udp) { + len += write_udp(spec, eth_encoder.ipv4_payload_view, eth_encoder); } - if(spec.tcp) { - len += write_tcp(spec, data.subarray(ihl * 4)); - } - if(spec.tcp_data) { - // TODO(perf) - for(let i = 0; i < spec.tcp_data.length; ++i) { - view.setUint8(len + i, spec.tcp_data[i]); - } - len += spec.tcp_data.length; + else if(spec.tcp) { + len += write_tcp(spec, eth_encoder.ipv4_payload_view); } view.setUint8(0, version << 4 | (ihl & 0x0F)); @@ -411,24 +619,10 @@ function write_ipv4(spec, data) { view.setUint8(6, 2 << 5); // DF Flag view.setUint8(8, spec.ipv4.ttl || 32); view.setUint8(9, spec.ipv4.proto); - view.setUint16(10, 0); // Checksum is zero during hashing - - for(let i = 0; i < 4; ++i) { - view.setUint8(12 + i, spec.ipv4.src[i]); - view.setUint8(16 + i, spec.ipv4.dest[i]); - } - - let checksum = 0; - for(let i = 0; i < ihl * 2; ++i) { - // TODO(perf) - checksum += view.getUint16(i << 1); - if(checksum > 0xFFFF) { - checksum = (checksum & 0xFFFF) + 1; - } - } - - view.setUint16(10, checksum ^ 0xFFFF); - + view.setUint16(10, 0); // checksum initially zero before calculation + view.setArray(12, spec.ipv4.src); + view.setArray(16, spec.ipv4.dest); + view.setInetChecksum(10, IPV4_HEADER_SIZE, 0); return len; } @@ -440,33 +634,17 @@ function parse_icmp(data, o) { checksum: view.getUint16(2), data: data.subarray(4) }; - o.icmp = icmp; - return true; } -function write_icmp(spec, data) { - let view = new DataView(data.buffer, data.byteOffset, data.byteLength); +function write_icmp(spec, view) { view.setUint8(0, spec.icmp.type); view.setUint8(1, spec.icmp.code); - view.setUint16(2, 0); // checksum 0 during calc - - for(let i = 0; i < spec.icmp.data.length; ++i) { - view.setUint8(i + 4, spec.icmp.data[i]); - } - - let checksum = 0; - for(let i = 0; i < 4 + spec.icmp.data.length; i += 2) { - // TODO(perf) - checksum += view.getUint16(i); - if(checksum > 0xFFFF) { - checksum = (checksum & 0xFFFF) + 1; - } - } - - view.setUint16(2, checksum ^ 0xFFFF); - - return 4 + spec.icmp.data.length; + view.setUint16(2, 0); // checksum initially zero before calculation + const data_length = view.setArray(ICMP_HEADER_SIZE, spec.icmp.data); + const total_length = ICMP_HEADER_SIZE + data_length; + view.setInetChecksum(2, total_length, 0); + return total_length; } function parse_udp(data, o) { @@ -484,41 +662,45 @@ function parse_udp(data, o) { if(udp.dport === 67 || udp.sport === 67) { //DHCP parse_dhcp(data.subarray(8), o); } - if(udp.dport === 53 || udp.sport === 53) { + else if(udp.dport === 53 || udp.sport === 53) { parse_dns(data.subarray(8), o); } - if(udp.dport === 123) { + else if(udp.dport === 123) { parse_ntp(data.subarray(8), o); } o.udp = udp; - return true; } -function write_udp(spec, data) { - let view = new DataView(data.buffer, data.byteOffset, data.byteLength); - +function write_udp(spec, view, eth_encoder) { let payload_length; - if(spec.dhcp) { - payload_length = write_dhcp(spec, data.subarray(8)); - } else if(spec.dns) { - payload_length = write_dns(spec, data.subarray(8)); - } else if(spec.ntp) { - payload_length = write_ntp(spec, data.subarray(8)); - } else { - let raw_data = spec.udp.data; - payload_length = raw_data.length; - for(let i = 0; i < raw_data.length; ++i) { - view.setUint8(8+i, raw_data[i]); - } + payload_length = write_dhcp(spec, eth_encoder.udp_payload_view); + } + else if(spec.dns) { + payload_length = write_dns(spec, eth_encoder.udp_payload_view); + } + else if(spec.ntp) { + payload_length = write_ntp(spec, eth_encoder.udp_payload_view); + } + else { + payload_length = eth_encoder.udp_payload_view.setArray(0, spec.udp.data); } + const total_length = UDP_HEADER_SIZE + payload_length; view.setUint16(0, spec.udp.sport); view.setUint16(2, spec.udp.dport); - view.setUint16(4, 8 + payload_length); - view.setUint16(6, 0); // Checksum - - return 8 + payload_length; + view.setUint16(4, total_length); + view.setUint16(6, 0); // checksum initially zero before calculation + + const pseudo_header = + (spec.ipv4.src[0] << 8 | spec.ipv4.src[1]) + + (spec.ipv4.src[2] << 8 | spec.ipv4.src[3]) + + (spec.ipv4.dest[0] << 8 | spec.ipv4.dest[1]) + + (spec.ipv4.dest[2] << 8 | spec.ipv4.dest[3]) + + IPV4_PROTO_UDP + + total_length; + view.setInetChecksum(6, total_length, pseudo_header); + return total_length; } function parse_dns(data, o) { @@ -572,8 +754,7 @@ function parse_dns(data, o) { o.dns = dns; } -function write_dns(spec, data) { - let view = new DataView(data.buffer, data.byteOffset, data.byteLength); +function write_dns(spec, view) { view.setUint16(0, spec.dns.id); view.setUint16(2, spec.dns.flags); view.setUint16(4, spec.dns.questions.length); @@ -583,12 +764,9 @@ function write_dns(spec, data) { for(let i = 0; i < spec.dns.questions.length; ++i) { let q = spec.dns.questions[i]; for(let s of q.name) { - view.setUint8(offset, s.length); - offset++; - for( let ii = 0; ii < s.length; ++ii) { - view.setUint8(offset, s.charCodeAt(ii)); - offset++; - } + const n_written = view.setString(offset + 1, s); + view.setUint8(offset, n_written); + offset += 1 + n_written; } view.setUint16(offset, q.type); offset += 2; @@ -598,12 +776,9 @@ function write_dns(spec, data) { function write_reply(a) { for(let s of a.name) { - view.setUint8(offset, s.length); - offset++; - for( let ii = 0; ii < s.length; ++ii) { - view.setUint8(offset, s.charCodeAt(ii)); - offset++; - } + const n_written = view.setString(offset + 1, s); + view.setUint8(offset, n_written); + offset += 1 + n_written; } view.setUint16(offset, a.type); offset += 2; @@ -613,12 +788,7 @@ function write_dns(spec, data) { offset += 4; view.setUint16(offset, a.data.length); offset += 2; - - for(let ii = 0; ii < a.data.length; ++ii) { - view.setUint8(offset + ii, a.data[ii]); - } - - offset += a.data.length; + offset += view.setArray(offset, a.data); } for(let i = 0; i < spec.dns.answers.length; ++i) { @@ -662,12 +832,9 @@ function parse_dhcp(data, o) { o.dhcp = dhcp; o.dhcp_options = dhcp.options; - return true; } -function write_dhcp(spec, data) { - let view = new DataView(data.buffer, data.byteOffset, data.byteLength); - +function write_dhcp(spec, view) { view.setUint8(0, spec.dhcp.op); view.setUint8(1, spec.dhcp.htype); view.setUint8(2, spec.dhcp.hlen); @@ -679,21 +846,14 @@ function write_dhcp(spec, data) { view.setUint32(16, spec.dhcp.yiaddr); view.setUint32(20, spec.dhcp.siaddr); view.setUint32(24, spec.dhcp.giaddr); - - for(let i = 0; i < spec.dhcp.chaddr.length; ++i) { - view.setUint8(28+i, spec.dhcp.chaddr[i]); - } + view.setArray(28, spec.dhcp.chaddr); view.setUint32(236, DHCP_MAGIC_COOKIE); let offset = 240; for(let o of spec.dhcp.options) { - for(let i = 0; i < o.length; ++i) { - view.setUint8(offset, o[i]); - ++offset; - } + offset += view.setArray(offset, o); } - return offset; } @@ -716,12 +876,9 @@ function parse_ntp(data, o) { trans_ts_i: view.getUint32(40), trans_ts_f: view.getUint32(44), }; - return true; } -function write_ntp(spec, data) { - let view = new DataView(data.buffer, data.byteOffset, data.byteLength); - +function write_ntp(spec, view) { view.setUint8(0, spec.ntp.flags); view.setUint8(1, spec.ntp.stratum); view.setUint8(2, spec.ntp.poll); @@ -737,12 +894,12 @@ function write_ntp(spec, data) { view.setUint32(36, spec.ntp.rec_ts_f); view.setUint32(40, spec.ntp.trans_ts_i); view.setUint32(44, spec.ntp.trans_ts_f); - return 48; } function parse_tcp(data, o) { let view = new DataView(data.buffer, data.byteOffset, data.byteLength); + let tcp = { sport: view.getUint16(0), dport: view.getUint16(2), @@ -769,12 +926,9 @@ function parse_tcp(data, o) { let offset = tcp.doff * 4; o.tcp_data = data.subarray(offset); - return true; } -function write_tcp(spec, data) { - let view = new DataView(data.buffer, data.byteOffset, data.byteLength); - +function write_tcp(spec, view) { let flags = 0; let tcp = spec.tcp; @@ -787,7 +941,7 @@ function write_tcp(spec, data) { if(tcp.ece) flags |= 0x40; if(tcp.cwr) flags |= 0x80; - let doff = 5; + const doff = TCP_HEADER_SIZE >> 2; // header length in 32-bit words view.setUint16(0, tcp.sport); view.setUint16(2, tcp.dport); @@ -796,67 +950,35 @@ function write_tcp(spec, data) { view.setUint8(12, doff << 4); view.setUint8(13, flags); view.setUint16(14, tcp.winsize); - view.setUint16(16, 0); // Checksum is 0 during calculation + view.setUint16(16, 0); // checksum initially zero before calculation view.setUint16(18, tcp.urgent || 0); - let total_len = (doff * 4) + (spec.tcp_data ? spec.tcp_data.length : 0); - - let checksum = 0; - let psudo_header = new Uint8Array(12); - let phview = new DataView(psudo_header.buffer, psudo_header.byteOffset, psudo_header.byteLength); - for(let i = 0; i < 4; ++i) { - phview.setUint8(i, spec.ipv4.src[i]); - phview.setUint8(4 + i, spec.ipv4.dest[i]); - } - phview.setUint8(9, IPV4_PROTO_TCP); - phview.setUint16(10, total_len); - - for(let i = 0; i < 6; ++i) { - // TODO(perf) - checksum += phview.getUint16(i << 1); - if(checksum > 0xFFFF) { - checksum = (checksum & 0xFFFF) + 1; - } - } - for(let i = 0; i < doff * 2; ++i) { - checksum += view.getUint16(i << 1); - if(checksum > 0xFFFF) { - checksum = (checksum & 0xFFFF) + 1; - } - } + const total_length = TCP_HEADER_SIZE + (spec.tcp_data ? spec.tcp_data.length : 0); if(spec.tcp_data) { - for(let i = 0; i < spec.tcp_data.length; i += 2) { - checksum += spec.tcp_data[i] << 8 | spec.tcp_data[i+1]; - if(checksum > 0xFFFF) { - checksum = (checksum & 0xFFFF) + 1; - } - } + view.setArray(TCP_HEADER_SIZE, spec.tcp_data); } - view.setUint16(16, checksum ^ 0xFFFF); - return doff * 4; -} - -function make_packet(spec) { - // TODO: Can we reuse this buffer? - let bytes = new Uint8Array(1518); // Max ethernet packet size - dbg_assert(spec.eth); - - let written = write_eth(spec, bytes); - return bytes.subarray(0, written); + const pseudo_header = + (spec.ipv4.src[0] << 8 | spec.ipv4.src[1]) + + (spec.ipv4.src[2] << 8 | spec.ipv4.src[3]) + + (spec.ipv4.dest[0] << 8 | spec.ipv4.dest[1]) + + (spec.ipv4.dest[2] << 8 | spec.ipv4.dest[3]) + + IPV4_PROTO_TCP + + total_length; + view.setInetChecksum(16, total_length, pseudo_header); + return total_length; } function fake_tcp_connect(dport, adapter) { - // TODO: check port collisions - let sport = 49152 + Math.floor(Math.random() * 1000); - let tuple = [ - adapter.vm_ip.join("."), - dport, - adapter.router_ip.join("."), - sport - ].join(":"); + const vm_ip_str = adapter.vm_ip.join("."); + const router_ip_str = adapter.router_ip.join("."); + let sport, tuple; + do { + sport = 1000 + Math.random() * 64535 | 0; + tuple = `${vm_ip_str}:${dport}:${router_ip_str}:${sport}`; + } while (adapter.tcp_conn.hasOwnProperty(tuple)); let reader; let connector; @@ -895,7 +1017,8 @@ function fake_tcp_connect(dport, adapter) */ function TCPConnection() { - this.send_buffer = new Uint8Array([]); + this.send_stream = new Uint8Stream(512, 0); + this.send_chunk_buf = new Uint8Array(TCP_PAYLOAD_SIZE); this.seq_history = []; } @@ -965,7 +1088,6 @@ TCPConnection.prototype.accept = function(packet) { }; TCPConnection.prototype.process = function(packet) { - // Receive Handshake Part 2, Send Part 3 if(packet.tcp.syn) { dbg_assert(packet.tcp.ack); @@ -1034,7 +1156,7 @@ TCPConnection.prototype.process = function(packet) { //console.log("Read ", nread, "(", this.last_received_ackn, ") ", packet.tcp.ackn, packet.tcp.winsize) if(nread > 0) { this.last_received_ackn = packet.tcp.ackn; - this.send_buffer = this.send_buffer.subarray(nread); + this.send_stream.remove(nread); this.seq += nread; this.pending = false; } @@ -1049,15 +1171,7 @@ TCPConnection.prototype.process = function(packet) { * @param {Uint8Array} data */ TCPConnection.prototype.write = function(data) { - if(this.send_buffer.length > 0) { - // TODO: Pretty inefficient - let concat = new Uint8Array(this.send_buffer.byteLength + data.byteLength); - concat.set(this.send_buffer, 0); - concat.set(data, this.send_buffer.byteLength); - this.send_buffer = concat; - } else { - this.send_buffer = data; - } + this.send_stream.write(data); this.pump(); }; @@ -1070,15 +1184,19 @@ TCPConnection.prototype.close = function() { }; TCPConnection.prototype.pump = function() { - - if(this.send_buffer.length > 0 && !this.pending) { - let data = this.send_buffer.subarray(0, 500); - let reply = this.ipv4_reply(); - + if (this.send_stream.length > 0 && !this.pending) { + const data = this.send_chunk_buf; + const n_ready = this.send_stream.peek(data, data.length); + const reply = this.ipv4_reply(); this.pending = true; - if(this.send_buffer.length < 1) reply.tcp.fin = true; +/* + if (this.send_stream.length - n_ready < 1) { +*/ + if (this.send_stream.length < 1) { // TODO: this can never be true!? + reply.tcp.fin = true; + } reply.tcp.psh = true; - reply.tcp_data = data; + reply.tcp_data = data.subarray(0, n_ready); this.net.receive(make_packet(reply)); } }; From a6d34c9ba84fb7bc7da39cb3e7943b3dfaf935b7 Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Mon, 4 Nov 2024 08:16:43 +0100 Subject: [PATCH 40/90] fixed eslint errors --- src/browser/fake_network.js | 44 ++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/browser/fake_network.js b/src/browser/fake_network.js index 83a995b481..391f114ca2 100644 --- a/src/browser/fake_network.js +++ b/src/browser/fake_network.js @@ -93,12 +93,12 @@ class Uint8Stream const src_length = src_array.length; const total_length = this.length + src_length; let capacity = this.buffer.length; - if (capacity < total_length) { - while (capacity < total_length) { + if(capacity < total_length) { + while(capacity < total_length) { capacity *= 2; } - if (this.maximum_capacity && capacity > this.maximum_capacity) { - console.error('stream capacity overflow in Uint8Stream.write()'); + if(this.maximum_capacity && capacity > this.maximum_capacity) { + console.error("stream capacity overflow in Uint8Stream.write()"); return; } this.resize(capacity); @@ -107,7 +107,7 @@ class Uint8Stream /* let head = this.head; - for (let i=0; i capacity) { + if(new_head > capacity) { const i_split = capacity - this.head; buffer.set(src_array.subarray(0, i_split), this.head); buffer.set(src_array.subarray(i_split)); @@ -133,20 +133,20 @@ class Uint8Stream */ peek(dst_array, length) { - if (length > this.length) { + if(length > this.length) { length = this.length; } - if (length) { + if(length) { const buffer = this.buffer; const capacity = buffer.length; /* - for (let i=0, tail = this.tail; i capacity) { + if(new_tail > capacity) { const buf_len_left = new_tail % capacity; const buf_len_right = capacity - this.tail; dst_array.set(buffer.subarray(this.tail)); @@ -164,10 +164,10 @@ class Uint8Stream */ remove(length) { - if (length > this.length) { + if(length > this.length) { length = this.length; } - if (length) { + if(length) { this.tail = (this.tail + length) % this.buffer.length; this.length -= length; } @@ -188,7 +188,7 @@ class EthernetPacketEncoder this.ipv4_payload_view = new DataView(eth_frame.buffer, offset + IPV4_PAYLOAD_OFFSET, IPV4_PAYLOAD_SIZE); this.udp_payload_view = new DataView(eth_frame.buffer, offset + UDP_PAYLOAD_OFFSET, UDP_PAYLOAD_SIZE); - for (const view of [this.eth_frame_view, this.eth_payload_view, this.ipv4_payload_view, this.udp_payload_view]) { + for(const view of [this.eth_frame_view, this.eth_payload_view, this.ipv4_payload_view, this.udp_payload_view]) { view.setArray = this.view_setArray.bind(this, view); view.setString = this.view_setString.bind(this, view); view.setInetChecksum = this.view_setInetChecksum.bind(this, view); @@ -238,13 +238,13 @@ class EthernetPacketEncoder const data = this.eth_frame; const offset = dst_view.byteOffset; const uint16_end = offset + (length & ~1); - for (let i = offset; i < uint16_end; i += 2) { + for(let i = offset; i < uint16_end; i += 2) { checksum += data[i] << 8 | data[i+1]; } - if (length & 1) { + if(length & 1) { checksum += data[uint16_end] << 8; } - while (checksum >> 16) { + while(checksum >> 16) { checksum = (checksum & 0xffff) + (checksum >> 16); } dst_view.setUint16(dst_offset, ~checksum); @@ -284,7 +284,7 @@ function handle_fake_tcp(packet, adapter) const tuple = `${packet.ipv4.src.join(".")}:${packet.tcp.sport}:${packet.ipv4.dest.join(".")}:${packet.tcp.dport}`; if(packet.tcp.syn) { - if(adapter.tcp_conn.hasOwnProperty(tuple)) { + if(adapter.tcp_conn[tuple]) { dbg_log("SYN to already opened port", LOG_FETCH); } if(adapter.on_tcp_connection(adapter, packet, tuple)) { @@ -292,7 +292,7 @@ function handle_fake_tcp(packet, adapter) } } - if(!adapter.tcp_conn.hasOwnProperty(tuple)) { + if(!adapter.tcp_conn[tuple]) { dbg_log(`I dont know about ${tuple}, so resetting`, LOG_FETCH); let bop = packet.tcp.ackn; if(packet.tcp.fin || packet.tcp.syn) bop += 1; @@ -978,7 +978,7 @@ function fake_tcp_connect(dport, adapter) do { sport = 1000 + Math.random() * 64535 | 0; tuple = `${vm_ip_str}:${dport}:${router_ip_str}:${sport}`; - } while (adapter.tcp_conn.hasOwnProperty(tuple)); + } while(adapter.tcp_conn[tuple]); let reader; let connector; @@ -1184,15 +1184,15 @@ TCPConnection.prototype.close = function() { }; TCPConnection.prototype.pump = function() { - if (this.send_stream.length > 0 && !this.pending) { + if(this.send_stream.length > 0 && !this.pending) { const data = this.send_chunk_buf; const n_ready = this.send_stream.peek(data, data.length); const reply = this.ipv4_reply(); this.pending = true; /* - if (this.send_stream.length - n_ready < 1) { + if(this.send_stream.length - n_ready < 1) { */ - if (this.send_stream.length < 1) { // TODO: this can never be true!? + if(this.send_stream.length < 1) { // TODO: this can never be true!? reply.tcp.fin = true; } reply.tcp.psh = true; From 80d427637e2d769132af15bbdf2e441ab310ff2c Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Mon, 4 Nov 2024 21:25:34 +0100 Subject: [PATCH 41/90] fixed FIN flag in generated TCP packets TCP FIN flag marks the sender's last package. This patch delays sending FIN in TCPConnection.close() until the send buffer is drained. --- src/browser/fake_network.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/browser/fake_network.js b/src/browser/fake_network.js index 391f114ca2..d6709c2ba3 100644 --- a/src/browser/fake_network.js +++ b/src/browser/fake_network.js @@ -1177,9 +1177,11 @@ TCPConnection.prototype.write = function(data) { TCPConnection.prototype.close = function() { this.state = TCP_STATE_FIN_WAIT_1; - let reply = this.ipv4_reply(); - reply.tcp.fin = true; - this.net.receive(make_packet(reply)); + if(!this.send_stream.length) { + let reply = this.ipv4_reply(); + reply.tcp.fin = true; + this.net.receive(make_packet(reply)); + } this.pump(); }; @@ -1189,10 +1191,7 @@ TCPConnection.prototype.pump = function() { const n_ready = this.send_stream.peek(data, data.length); const reply = this.ipv4_reply(); this.pending = true; -/* - if(this.send_stream.length - n_ready < 1) { -*/ - if(this.send_stream.length < 1) { // TODO: this can never be true!? + if(this.state === TCP_STATE_FIN_WAIT_1 && this.send_stream.length - n_ready === 0) { reply.tcp.fin = true; } reply.tcp.psh = true; From c9c98760b38fc5863103b2d9e233ba1cd67856cc Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Mon, 4 Nov 2024 21:42:27 +0100 Subject: [PATCH 42/90] minor fixes --- src/browser/fake_network.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/browser/fake_network.js b/src/browser/fake_network.js index d6709c2ba3..4b06b1446d 100644 --- a/src/browser/fake_network.js +++ b/src/browser/fake_network.js @@ -1017,7 +1017,7 @@ function fake_tcp_connect(dport, adapter) */ function TCPConnection() { - this.send_stream = new Uint8Stream(512, 0); + this.send_stream = new Uint8Stream(2048, 0); this.send_chunk_buf = new Uint8Array(TCP_PAYLOAD_SIZE); this.seq_history = []; } @@ -1191,7 +1191,7 @@ TCPConnection.prototype.pump = function() { const n_ready = this.send_stream.peek(data, data.length); const reply = this.ipv4_reply(); this.pending = true; - if(this.state === TCP_STATE_FIN_WAIT_1 && this.send_stream.length - n_ready === 0) { + if(this.state === TCP_STATE_FIN_WAIT_1 && this.send_stream.length === n_ready) { reply.tcp.fin = true; } reply.tcp.psh = true; From 02996b04aefb81c5053192d18fa2cc778d4d540b Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Tue, 5 Nov 2024 21:02:32 +0100 Subject: [PATCH 43/90] several improvements to GrowableRingbuffer (former class Uint8Stream) - Renamed Uint8Stream to GrowableRingbuffer - Minimum capacity now guaranteed to be 16, and maximum (if given) to be greater or equal to minumum - Changed peek(dst_array, length) to peek(dst_array) - Removed misleading method resize(), moved code into write() - Now throws an Exception on capacity overflow error instead of console.error() - Removed debug code that is no longer needed When we have a capacity overflow the situation is as bad as if the Uint8Array constructor had failed with out-of-memory, which throws a RangeError. --- src/browser/fake_network.js | 52 ++++++++++--------------------------- 1 file changed, 13 insertions(+), 39 deletions(-) diff --git a/src/browser/fake_network.js b/src/browser/fake_network.js index 4b06b1446d..93ae3f13b1 100644 --- a/src/browser/fake_network.js +++ b/src/browser/fake_network.js @@ -58,7 +58,7 @@ function iptolong(parts) { return parts[0] << 24 | parts[1] << 16 | parts[2] << 8 | parts[3]; } -class Uint8Stream +class GrowableRingbuffer { /** * @param {number} initial_capacity @@ -66,25 +66,14 @@ class Uint8Stream */ constructor(initial_capacity, maximum_capacity) { - this.maximum_capacity = maximum_capacity; + initial_capacity = Math.min(initial_capacity, 16); + this.maximum_capacity = maximum_capacity ? Math.max(maximum_capacity, initial_capacity) : 0; this.tail = 0; this.head = 0; this.length = 0; this.buffer = new Uint8Array(initial_capacity); } - /** - * @param {number} capacity - */ - resize(capacity) - { - const new_buffer = new Uint8Array(capacity); - this.peek(new_buffer, this.length); - this.tail = 0; - this.head = this.length; - this.buffer = new_buffer; - } - /** * @param {Uint8Array} src_array */ @@ -98,22 +87,16 @@ class Uint8Stream capacity *= 2; } if(this.maximum_capacity && capacity > this.maximum_capacity) { - console.error("stream capacity overflow in Uint8Stream.write()"); - return; + throw new Error("stream capacity overflow in GrowableRingbuffer.write(), package dropped"); } - this.resize(capacity); + const new_buffer = new Uint8Array(capacity); + this.peek(new_buffer); + this.tail = 0; + this.head = this.length; + this.buffer = new_buffer; } const buffer = this.buffer; - /* - let head = this.head; - for(let i=0; i capacity) { const i_split = capacity - this.head; @@ -129,22 +112,13 @@ class Uint8Stream /** * @param {Uint8Array} dst_array - * @param {number} length */ - peek(dst_array, length) + peek(dst_array) { - if(length > this.length) { - length = this.length; - } + const length = Math.min(this.length, dst_array.length); if(length) { const buffer = this.buffer; const capacity = buffer.length; - /* - for(let i=0, tail = this.tail; i capacity) { const buf_len_left = new_tail % capacity; @@ -1017,7 +991,7 @@ function fake_tcp_connect(dport, adapter) */ function TCPConnection() { - this.send_stream = new Uint8Stream(2048, 0); + this.send_stream = new GrowableRingbuffer(2048, 0); this.send_chunk_buf = new Uint8Array(TCP_PAYLOAD_SIZE); this.seq_history = []; } @@ -1188,7 +1162,7 @@ TCPConnection.prototype.close = function() { TCPConnection.prototype.pump = function() { if(this.send_stream.length > 0 && !this.pending) { const data = this.send_chunk_buf; - const n_ready = this.send_stream.peek(data, data.length); + const n_ready = this.send_stream.peek(data); const reply = this.ipv4_reply(); this.pending = true; if(this.state === TCP_STATE_FIN_WAIT_1 && this.send_stream.length === n_ready) { From 4e95c62e01f760f54af4e2c02aa717b3493189e7 Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Tue, 5 Nov 2024 21:55:29 +0100 Subject: [PATCH 44/90] fixed finding unused dynamic TCP port - use actual dynamic TCP port range from RFC6335 - throw an Exception when pool of dynamic ports is exhausted --- src/browser/fake_network.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/browser/fake_network.js b/src/browser/fake_network.js index 93ae3f13b1..c1a34bccb8 100644 --- a/src/browser/fake_network.js +++ b/src/browser/fake_network.js @@ -29,6 +29,11 @@ const TCP_STATE_FIN_WAIT_1 = "fin-wait-1"; // close() (graceful-shutdown in const TCP_STATE_SYN_RECEIVED = "syn-received"; // WispNetworkAdapter.send() (wisp_network.js) const TCP_STATE_SYN_SENT = "syn-sent"; // connect() + process() +// source: RFC6335, 6. Port Number Ranges +const TCP_DYNAMIC_PORT_START = 49152; +const TCP_DYNAMIC_PORT_END = 65535; +const TCP_DYNAMIC_PORT_RANGE = TCP_DYNAMIC_PORT_END - TCP_DYNAMIC_PORT_START; + const ETH_HEADER_SIZE = 14; const ETH_PAYLOAD_OFFSET = ETH_HEADER_SIZE; const ETH_PAYLOAD_SIZE = 1500; @@ -948,11 +953,15 @@ function fake_tcp_connect(dport, adapter) { const vm_ip_str = adapter.vm_ip.join("."); const router_ip_str = adapter.router_ip.join("."); - let sport, tuple; + const sport_0 = (Math.random() * TCP_DYNAMIC_PORT_RANGE) | 0; + let sport, tuple, sport_i = 0; do { - sport = 1000 + Math.random() * 64535 | 0; + sport = TCP_DYNAMIC_PORT_START + ((sport_0 + sport_i) % TCP_DYNAMIC_PORT_RANGE); tuple = `${vm_ip_str}:${dport}:${router_ip_str}:${sport}`; - } while(adapter.tcp_conn[tuple]); + } while(++sport_i < TCP_DYNAMIC_PORT_RANGE && adapter.tcp_conn[tuple]); + if(adapter.tcp_conn[tuple]) { + throw new Error("pool of dynamic TCP port numbers exhausted, connection aborted"); + } let reader; let connector; From ae23b97a2a6f392e158c8ee0e25378563f2b5c3e Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Wed, 6 Nov 2024 00:30:30 +0100 Subject: [PATCH 45/90] removed class EthernetPacketEncoder - removed class EthernetPacketEncoder and its global single instance ethernet_encoder - moved encoder buffer ownership to adapter (WispNetworkAdapter and FetchNetworkAdapter) - made view_{setArray,setString,setInetChecksum} global functions - changed prototype of all write_...(spec, ...) functions to uniform write_...(spec, out) - removed function make_packet(spec), added new function adapter_receive(adapter, spec) - replaced all calls of form "A.receive(make_packet(S))" with "adapter_receive(A, S)" (also in WispNetworkAdapter) Note that before this patch function make_packet() was always called in the form: adapter.receive(make_packet(spec)); This patch binds the lifetime and scope of the encoder buffer from class EthernetPacketEncoder to the WispNetworkAdapter/FetchNetworkAdapter instance "adapter". --- src/browser/fake_network.js | 267 +++++++++++++++++------------------ src/browser/fetch_network.js | 1 + src/browser/wisp_network.js | 5 +- 3 files changed, 134 insertions(+), 139 deletions(-) diff --git a/src/browser/fake_network.js b/src/browser/fake_network.js index c1a34bccb8..cba5912167 100644 --- a/src/browser/fake_network.js +++ b/src/browser/fake_network.js @@ -154,101 +154,85 @@ class GrowableRingbuffer } } -class EthernetPacketEncoder +function create_eth_encoder_buf() { - constructor() - { - const eth_frame = new Uint8Array(ETH_FRAME_SIZE); - const offset = eth_frame.byteOffset; - - this.eth_frame = eth_frame; - this.eth_frame_view = new DataView(eth_frame.buffer); - this.eth_payload_view = new DataView(eth_frame.buffer, offset + ETH_PAYLOAD_OFFSET, ETH_PAYLOAD_SIZE); - this.ipv4_payload_view = new DataView(eth_frame.buffer, offset + IPV4_PAYLOAD_OFFSET, IPV4_PAYLOAD_SIZE); - this.udp_payload_view = new DataView(eth_frame.buffer, offset + UDP_PAYLOAD_OFFSET, UDP_PAYLOAD_SIZE); - - for(const view of [this.eth_frame_view, this.eth_payload_view, this.ipv4_payload_view, this.udp_payload_view]) { - view.setArray = this.view_setArray.bind(this, view); - view.setString = this.view_setString.bind(this, view); - view.setInetChecksum = this.view_setInetChecksum.bind(this, view); - } + const eth_frame = new Uint8Array(ETH_FRAME_SIZE); + const buffer = eth_frame.buffer; + const offset = eth_frame.byteOffset; + return { + eth_frame: eth_frame, + eth_frame_view: new DataView(buffer), + eth_payload_view: new DataView(buffer, offset + ETH_PAYLOAD_OFFSET, ETH_PAYLOAD_SIZE), + ipv4_payload_view: new DataView(buffer, offset + IPV4_PAYLOAD_OFFSET, IPV4_PAYLOAD_SIZE), + udp_payload_view: new DataView(buffer, offset + UDP_PAYLOAD_OFFSET, UDP_PAYLOAD_SIZE), + text_encoder: new TextEncoder() + }; +} - this.text_encoder = new TextEncoder(); - } +/** + * Copy given data array into view starting at offset, return number of bytes written. + * + * @param {number} offset + * @param {ArrayBuffer|ArrayBufferView} data + * @param {DataView} view + * @param {Object} out + */ +function view_setArray(offset, data, view, out) +{ + out.eth_frame.set(data, view.byteOffset + offset); + return data.length; +} - /** - * Copy given data array into dst_view[dst_offset], return number of bytes written. - * - * @param {DataView} dst_view - * @param {number} dst_offset - * @param data - */ - view_setArray(dst_view, dst_offset, data) - { - this.eth_frame.set(data, dst_view.byteOffset + dst_offset); - return data.length; - } +/** + * UTF8-encode given string into view starting at offset, return number of bytes written. + * + * @param {number} offset + * @param {string} str + * @param {DataView} view + * @param {Object} out + */ +function view_setString(offset, str, view, out) +{ + const ofs = view.byteOffset + offset; + const result = out.text_encoder.encodeInto(str, ofs ? out.eth_frame.subarray(ofs) : out.eth_frame); + return result.written; +} - /** - * UTF8-encode given string into dst_view[dst_offset], return number of bytes written. - * - * @param {DataView} dst_view - * @param {number} dst_offset - * @param {string} str - */ - view_setString(dst_view, dst_offset, str) - { - const ofs = dst_view.byteOffset + dst_offset; - const result = this.text_encoder.encodeInto(str, ofs ? this.eth_frame.subarray(ofs) : this.eth_frame); - return result.written; +/** + * Calculate internet checksum for view[0 : length], store 16-bit result in view at offset. + * Source: RFC768 and RFC1071 (chapter 4.1). + * + * @param {number} offset + * @param {number} length + * @param {number} checksum + * @param {DataView} view + * @param {Object} out + */ +function view_setInetChecksum(offset, length, checksum, view, out) +{ + const uint16_end = view.byteOffset + (length & ~1); + for(let i = view.byteOffset; i < uint16_end; i += 2) { + checksum += out.eth_frame[i] << 8 | out.eth_frame[i+1]; } - - /** - * Calculate 16-bit internet checksum for dst_view[0 : length], store result in dst_view[dst_offset]. - * Source: RFC768 and RFC1071 (chapter 4.1). - * - * @param {DataView} dst_view - * @param {number} dst_offset - * @param {number} length - * @param {number} checksum - */ - view_setInetChecksum(dst_view, dst_offset, length, checksum) - { - const data = this.eth_frame; - const offset = dst_view.byteOffset; - const uint16_end = offset + (length & ~1); - for(let i = offset; i < uint16_end; i += 2) { - checksum += data[i] << 8 | data[i+1]; - } - if(length & 1) { - checksum += data[uint16_end] << 8; - } - while(checksum >> 16) { - checksum = (checksum & 0xffff) + (checksum >> 16); - } - dst_view.setUint16(dst_offset, ~checksum); + if(length & 1) { + checksum += out.eth_frame[uint16_end] << 8; } - - /** - * Encode and return ethernet packet for given spec. - * TODO: what about the trailing 32-bit ethernet checksum? - * - * @param {Object} spec - */ - encode_packet(spec) - { - dbg_assert(spec.eth); - this.eth_frame.fill(0); - const length = write_eth(spec, this.eth_frame_view, this); - return this.eth_frame.subarray(0, length); + while(checksum >> 16) { + checksum = (checksum & 0xffff) + (checksum >> 16); } + view.setUint16(offset, ~checksum); } -const ethernet_encoder = new EthernetPacketEncoder(); - -function make_packet(spec) +/** + * @param {WispNetworkAdapter|FetchNetworkAdapter} adapter + * @param {Object} spec + */ +function adapter_receive(adapter, spec) { - return ethernet_encoder.encode_packet(spec); + dbg_assert(spec.eth); + const out = adapter.eth_encoder_buf; + out.eth_frame.fill(0); + adapter.receive(out.eth_frame.subarray(0, write_eth(spec, out))); } function handle_fake_tcp(packet, adapter) @@ -284,7 +268,7 @@ function handle_fake_tcp(packet, adapter) rst: true, ack: packet.tcp.syn }; - adapter.receive(make_packet(reply)); + adapter_receive(adapter, reply); return true; } @@ -330,7 +314,7 @@ function handle_fake_dns(packet, adapter) questions: packet.dns.questions, answers: answers }; - adapter.receive(make_packet(reply)); + adapter_receive(adapter, reply); return true; } @@ -361,7 +345,7 @@ function handle_fake_ntp(packet, adapter) { reply.ntp.trans_ts_f = now_n_f; reply.ntp.stratum = 2; - adapter.receive(make_packet(reply)); + adapter_receive(adapter, reply); return true; } @@ -416,7 +400,7 @@ function handle_fake_dhcp(packet, adapter) { options.push(new Uint8Array([255, 0])); reply.dhcp.options = options; - adapter.receive(make_packet(reply)); + adapter_receive(adapter, reply); } function handle_fake_networking(data, adapter) { @@ -491,16 +475,17 @@ function parse_eth(data, o) { } } -function write_eth(spec, view, eth_encoder) { - view.setArray(0, spec.eth.dest); - view.setArray(6, spec.eth.src); +function write_eth(spec, out) { + const view = out.eth_frame_view; + view_setArray(0, spec.eth.dest, view, out); + view_setArray(6, spec.eth.src, view, out); view.setUint16(12, spec.eth.ethertype); let len = ETH_HEADER_SIZE; if(spec.arp) { - len += write_arp(spec, eth_encoder.eth_payload_view); + len += write_arp(spec, out); } else if(spec.ipv4) { - len += write_ipv4(spec, eth_encoder.eth_payload_view, eth_encoder); + len += write_ipv4(spec, out); } return len; } @@ -522,16 +507,17 @@ function parse_arp(data, o) { o.arp = arp; } -function write_arp(spec, view) { +function write_arp(spec, out) { + const view = out.eth_payload_view; view.setUint16(0, spec.arp.htype); view.setUint16(2, spec.arp.ptype); view.setUint8(4, spec.arp.sha.length); view.setUint8(5, spec.arp.spa.length); view.setUint16(6, spec.arp.oper); - view.setArray(8, spec.arp.sha); - view.setArray(14, spec.arp.spa); - view.setArray(18, spec.arp.tha); - view.setArray(24, spec.arp.tpa); + view_setArray(8, spec.arp.sha, view, out); + view_setArray(14, spec.arp.spa, view, out); + view_setArray(18, spec.arp.tha, view, out); + view_setArray(24, spec.arp.tpa, view, out); return 28; } @@ -576,19 +562,20 @@ function parse_ipv4(data, o) { } } -function write_ipv4(spec, view, eth_encoder) { +function write_ipv4(spec, out) { + const view = out.eth_payload_view; const ihl = IPV4_HEADER_SIZE >> 2; // header length in 32-bit words const version = 4; let len = IPV4_HEADER_SIZE; if(spec.icmp) { - len += write_icmp(spec, eth_encoder.ipv4_payload_view); + len += write_icmp(spec, out); } else if(spec.udp) { - len += write_udp(spec, eth_encoder.ipv4_payload_view, eth_encoder); + len += write_udp(spec, out); } else if(spec.tcp) { - len += write_tcp(spec, eth_encoder.ipv4_payload_view); + len += write_tcp(spec, out); } view.setUint8(0, version << 4 | (ihl & 0x0F)); @@ -599,9 +586,9 @@ function write_ipv4(spec, view, eth_encoder) { view.setUint8(8, spec.ipv4.ttl || 32); view.setUint8(9, spec.ipv4.proto); view.setUint16(10, 0); // checksum initially zero before calculation - view.setArray(12, spec.ipv4.src); - view.setArray(16, spec.ipv4.dest); - view.setInetChecksum(10, IPV4_HEADER_SIZE, 0); + view_setArray(12, spec.ipv4.src, view, out); + view_setArray(16, spec.ipv4.dest, view, out); + view_setInetChecksum(10, IPV4_HEADER_SIZE, 0, view, out); return len; } @@ -616,13 +603,14 @@ function parse_icmp(data, o) { o.icmp = icmp; } -function write_icmp(spec, view) { +function write_icmp(spec, out) { + const view = out.ipv4_payload_view; view.setUint8(0, spec.icmp.type); view.setUint8(1, spec.icmp.code); view.setUint16(2, 0); // checksum initially zero before calculation - const data_length = view.setArray(ICMP_HEADER_SIZE, spec.icmp.data); + const data_length = view_setArray(ICMP_HEADER_SIZE, spec.icmp.data, view, out); const total_length = ICMP_HEADER_SIZE + data_length; - view.setInetChecksum(2, total_length, 0); + view_setInetChecksum(2, total_length, 0, view, out); return total_length; } @@ -650,19 +638,20 @@ function parse_udp(data, o) { o.udp = udp; } -function write_udp(spec, view, eth_encoder) { +function write_udp(spec, out) { + const view = out.ipv4_payload_view; let payload_length; if(spec.dhcp) { - payload_length = write_dhcp(spec, eth_encoder.udp_payload_view); + payload_length = write_dhcp(spec, out); } else if(spec.dns) { - payload_length = write_dns(spec, eth_encoder.udp_payload_view); + payload_length = write_dns(spec, out); } else if(spec.ntp) { - payload_length = write_ntp(spec, eth_encoder.udp_payload_view); + payload_length = write_ntp(spec, out); } else { - payload_length = eth_encoder.udp_payload_view.setArray(0, spec.udp.data); + payload_length = view_setArray(0, spec.udp.data, out.udp_payload_view, out); } const total_length = UDP_HEADER_SIZE + payload_length; @@ -678,7 +667,7 @@ function write_udp(spec, view, eth_encoder) { (spec.ipv4.dest[2] << 8 | spec.ipv4.dest[3]) + IPV4_PROTO_UDP + total_length; - view.setInetChecksum(6, total_length, pseudo_header); + view_setInetChecksum(6, total_length, pseudo_header, view, out); return total_length; } @@ -733,7 +722,8 @@ function parse_dns(data, o) { o.dns = dns; } -function write_dns(spec, view) { +function write_dns(spec, out) { + const view = out.udp_payload_view; view.setUint16(0, spec.dns.id); view.setUint16(2, spec.dns.flags); view.setUint16(4, spec.dns.questions.length); @@ -743,7 +733,7 @@ function write_dns(spec, view) { for(let i = 0; i < spec.dns.questions.length; ++i) { let q = spec.dns.questions[i]; for(let s of q.name) { - const n_written = view.setString(offset + 1, s); + const n_written = view_setString(offset + 1, s, view, out); view.setUint8(offset, n_written); offset += 1 + n_written; } @@ -755,7 +745,7 @@ function write_dns(spec, view) { function write_reply(a) { for(let s of a.name) { - const n_written = view.setString(offset + 1, s); + const n_written = view_setString(offset + 1, s, view, out); view.setUint8(offset, n_written); offset += 1 + n_written; } @@ -767,7 +757,7 @@ function write_dns(spec, view) { offset += 4; view.setUint16(offset, a.data.length); offset += 2; - offset += view.setArray(offset, a.data); + offset += view_setArray(offset, a.data, view, out); } for(let i = 0; i < spec.dns.answers.length; ++i) { @@ -813,7 +803,8 @@ function parse_dhcp(data, o) { o.dhcp_options = dhcp.options; } -function write_dhcp(spec, view) { +function write_dhcp(spec, out) { + const view = out.udp_payload_view; view.setUint8(0, spec.dhcp.op); view.setUint8(1, spec.dhcp.htype); view.setUint8(2, spec.dhcp.hlen); @@ -825,13 +816,13 @@ function write_dhcp(spec, view) { view.setUint32(16, spec.dhcp.yiaddr); view.setUint32(20, spec.dhcp.siaddr); view.setUint32(24, spec.dhcp.giaddr); - view.setArray(28, spec.dhcp.chaddr); + view_setArray(28, spec.dhcp.chaddr, view, out); view.setUint32(236, DHCP_MAGIC_COOKIE); let offset = 240; for(let o of spec.dhcp.options) { - offset += view.setArray(offset, o); + offset += view_setArray(offset, o, view, out); } return offset; } @@ -857,7 +848,8 @@ function parse_ntp(data, o) { }; } -function write_ntp(spec, view) { +function write_ntp(spec, out) { + const view = out.udp_payload_view; view.setUint8(0, spec.ntp.flags); view.setUint8(1, spec.ntp.stratum); view.setUint8(2, spec.ntp.poll); @@ -907,7 +899,8 @@ function parse_tcp(data, o) { o.tcp_data = data.subarray(offset); } -function write_tcp(spec, view) { +function write_tcp(spec, out) { + const view = out.ipv4_payload_view; let flags = 0; let tcp = spec.tcp; @@ -935,7 +928,7 @@ function write_tcp(spec, view) { const total_length = TCP_HEADER_SIZE + (spec.tcp_data ? spec.tcp_data.length : 0); if(spec.tcp_data) { - view.setArray(TCP_HEADER_SIZE, spec.tcp_data); + view_setArray(TCP_HEADER_SIZE, spec.tcp_data, view, out); } const pseudo_header = @@ -945,7 +938,7 @@ function write_tcp(spec, view) { (spec.ipv4.dest[2] << 8 | spec.ipv4.dest[3]) + IPV4_PROTO_TCP + total_length; - view.setInetChecksum(16, total_length, pseudo_header); + view_setInetChecksum(16, total_length, pseudo_header, view, out); return total_length; } @@ -1041,7 +1034,7 @@ TCPConnection.prototype.connect = function() { winsize: 0, syn: true, }; - this.net.receive(make_packet(reply)); + adapter_receive(this.net, reply); }; TCPConnection.prototype.accept = function(packet) { @@ -1067,7 +1060,7 @@ TCPConnection.prototype.accept = function(packet) { syn: true, ack: true }; - this.net.receive(make_packet(reply)); + adapter_receive(this.net, reply); }; TCPConnection.prototype.process = function(packet) { @@ -1081,7 +1074,7 @@ TCPConnection.prototype.process = function(packet) { this.last_received_ackn = packet.tcp.ackn; let reply = this.ipv4_reply(); - this.net.receive(make_packet(reply)); + adapter_receive(this.net, reply); this.state = TCP_STATE_ESTABLISHED; if(this.on_connect) this.on_connect.call(this); @@ -1104,7 +1097,7 @@ TCPConnection.prototype.process = function(packet) { rst: true, }; delete this.net.tcp_conn[this.tuple]; - this.net.receive(make_packet(reply)); + adapter_receive(this.net, reply); return; } @@ -1120,7 +1113,7 @@ TCPConnection.prototype.process = function(packet) { winsize: packet.tcp.winsize, ack: true }; - this.net.receive(make_packet(reply)); + adapter_receive(this.net, reply); return; } @@ -1131,7 +1124,7 @@ TCPConnection.prototype.process = function(packet) { if(packet.tcp_data.length > 0) { let reply = this.ipv4_reply(); - this.net.receive(make_packet(reply)); + adapter_receive(this.net, reply); } if(this.last_received_ackn === undefined) this.last_received_ackn = packet.tcp.ackn; @@ -1163,7 +1156,7 @@ TCPConnection.prototype.close = function() { if(!this.send_stream.length) { let reply = this.ipv4_reply(); reply.tcp.fin = true; - this.net.receive(make_packet(reply)); + adapter_receive(this.net, reply); } this.pump(); }; @@ -1179,7 +1172,7 @@ TCPConnection.prototype.pump = function() { } reply.tcp.psh = true; reply.tcp_data = data.subarray(0, n_ready); - this.net.receive(make_packet(reply)); + adapter_receive(this.net, reply); } }; @@ -1211,7 +1204,7 @@ function arp_whohas(packet, adapter) { tha: packet.eth.src, tpa: packet.arp.spa }; - adapter.receive(make_packet(reply)); + adapter_receive(adapter, reply); } function handle_fake_ping(packet, adapter) { @@ -1227,7 +1220,7 @@ function handle_fake_ping(packet, adapter) { code: packet.icmp.code, data: packet.icmp.data }; - adapter.receive(make_packet(reply)); + adapter_receive(adapter, reply); } function handle_udp_echo(packet, adapter) { @@ -1244,5 +1237,5 @@ function handle_udp_echo(packet, adapter) { dport: packet.udp.sport, data: new TextEncoder().encode(packet.udp.data_s) }; - adapter.receive(make_packet(reply)); + adapter_receive(adapter, reply); } diff --git a/src/browser/fetch_network.js b/src/browser/fetch_network.js index 2314a20724..0a8b5f7fa5 100644 --- a/src/browser/fetch_network.js +++ b/src/browser/fetch_network.js @@ -18,6 +18,7 @@ function FetchNetworkAdapter(bus, config) this.vm_mac = new Uint8Array(6); this.tcp_conn = {}; + this.eth_encoder_buf = create_eth_encoder_buf(); // Ex: 'https://corsproxy.io/?' this.cors_proxy = config.cors_proxy; diff --git a/src/browser/wisp_network.js b/src/browser/wisp_network.js index faf0f5fc8c..4ecd8c2caa 100644 --- a/src/browser/wisp_network.js +++ b/src/browser/wisp_network.js @@ -26,6 +26,7 @@ function WispNetworkAdapter(wisp_url, bus, config) this.vm_mac = new Uint8Array(6); this.doh_server = config.doh_server || DEFAULT_DOH_SERVER; this.tcp_conn = {}; + this.eth_encoder_buf = create_eth_encoder_buf(); this.bus.register("net" + this.id + "-mac", function(mac) { this.vm_mac = new Uint8Array(mac.split(":").map(function(x) { return parseInt(x, 16); })); @@ -248,7 +249,7 @@ WispNetworkAdapter.prototype.send = function(data) rst: true, ack: packet.tcp.syn }; - this.receive(make_packet(reply)); + adapter_receive(this, reply); return; } @@ -272,7 +273,7 @@ WispNetworkAdapter.prototype.send = function(data) reply.udp = { sport: 53, dport: packet.udp.sport }; const result = await ((await fetch(`https://${this.doh_server}/dns-query`, {method: "POST", headers: [["content-type", "application/dns-message"]], body: packet.udp.data})).arrayBuffer()); reply.udp.data = new Uint8Array(result); - this.receive(make_packet(reply)); + adapter_receive(this, reply); })(); } From 6b5cfc1b461e6f9118be12ae85995acc074711b7 Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Wed, 6 Nov 2024 00:54:38 +0100 Subject: [PATCH 46/90] minor cleanups and alignments - removed unnecessary comments around TCP_STATE_... - renamed TCPConnection.send_stream to send_buffer - calculate package length in write_udp() and write_tcp() along the same structure as in all other write_...() functions --- src/browser/fake_network.js | 52 ++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/src/browser/fake_network.js b/src/browser/fake_network.js index cba5912167..759e14226b 100644 --- a/src/browser/fake_network.js +++ b/src/browser/fake_network.js @@ -17,17 +17,17 @@ const TWO_TO_32 = Math.pow(2, 32); const DHCP_MAGIC_COOKIE = 0x63825363; const V86_ASCII = [118, 56, 54]; -//const TCP_STATE_CLOSED = "closed"; // unused (terminal state) -//const TCP_STATE_LISTEN = "listen"; // unused -const TCP_STATE_ESTABLISHED = "established"; // TCPConnection.process() -const TCP_STATE_FIN_WAIT_1 = "fin-wait-1"; // close() (graceful-shutdown initiator state) -//const TCP_STATE_CLOSE_WAIT = "close-wait"; // unused (graceful-shutdown receiver state) -//const TCP_STATE_FIN_WAIT_2 = "fin-wait-2"; // unused (graceful-shutdown initiator state) -//const TCP_STATE_LAST_ACK = "last-ack"; // unused (graceful-shutdown receiver state) -//const TCP_STATE_CLOSING = "closing"; // unused -//const TCP_STATE_TIME_WAIT = "time-wait"; // unused (graceful-shutdown initiator state) -const TCP_STATE_SYN_RECEIVED = "syn-received"; // WispNetworkAdapter.send() (wisp_network.js) -const TCP_STATE_SYN_SENT = "syn-sent"; // connect() + process() +//const TCP_STATE_CLOSED = "closed"; +//const TCP_STATE_LISTEN = "listen"; +const TCP_STATE_ESTABLISHED = "established"; +const TCP_STATE_FIN_WAIT_1 = "fin-wait-1"; +//const TCP_STATE_CLOSE_WAIT = "close-wait"; +//const TCP_STATE_FIN_WAIT_2 = "fin-wait-2"; +//const TCP_STATE_LAST_ACK = "last-ack"; +//const TCP_STATE_CLOSING = "closing"; +//const TCP_STATE_TIME_WAIT = "time-wait"; +const TCP_STATE_SYN_RECEIVED = "syn-received"; +const TCP_STATE_SYN_SENT = "syn-sent"; // source: RFC6335, 6. Port Number Ranges const TCP_DYNAMIC_PORT_START = 49152; @@ -640,21 +640,20 @@ function parse_udp(data, o) { function write_udp(spec, out) { const view = out.ipv4_payload_view; - let payload_length; + let total_length = UDP_HEADER_SIZE; if(spec.dhcp) { - payload_length = write_dhcp(spec, out); + total_length += write_dhcp(spec, out); } else if(spec.dns) { - payload_length = write_dns(spec, out); + total_length += write_dns(spec, out); } else if(spec.ntp) { - payload_length = write_ntp(spec, out); + total_length += write_ntp(spec, out); } else { - payload_length = view_setArray(0, spec.udp.data, out.udp_payload_view, out); + total_length += view_setArray(0, spec.udp.data, out.udp_payload_view, out); } - const total_length = UDP_HEADER_SIZE + payload_length; view.setUint16(0, spec.udp.sport); view.setUint16(2, spec.udp.dport); view.setUint16(4, total_length); @@ -925,10 +924,9 @@ function write_tcp(spec, out) { view.setUint16(16, 0); // checksum initially zero before calculation view.setUint16(18, tcp.urgent || 0); - const total_length = TCP_HEADER_SIZE + (spec.tcp_data ? spec.tcp_data.length : 0); - + let total_length = TCP_HEADER_SIZE; if(spec.tcp_data) { - view_setArray(TCP_HEADER_SIZE, spec.tcp_data, view, out); + total_length += view_setArray(TCP_HEADER_SIZE, spec.tcp_data, view, out); } const pseudo_header = @@ -993,7 +991,7 @@ function fake_tcp_connect(dport, adapter) */ function TCPConnection() { - this.send_stream = new GrowableRingbuffer(2048, 0); + this.send_buffer = new GrowableRingbuffer(2048, 0); this.send_chunk_buf = new Uint8Array(TCP_PAYLOAD_SIZE); this.seq_history = []; } @@ -1132,7 +1130,7 @@ TCPConnection.prototype.process = function(packet) { //console.log("Read ", nread, "(", this.last_received_ackn, ") ", packet.tcp.ackn, packet.tcp.winsize) if(nread > 0) { this.last_received_ackn = packet.tcp.ackn; - this.send_stream.remove(nread); + this.send_buffer.remove(nread); this.seq += nread; this.pending = false; } @@ -1147,13 +1145,13 @@ TCPConnection.prototype.process = function(packet) { * @param {Uint8Array} data */ TCPConnection.prototype.write = function(data) { - this.send_stream.write(data); + this.send_buffer.write(data); this.pump(); }; TCPConnection.prototype.close = function() { this.state = TCP_STATE_FIN_WAIT_1; - if(!this.send_stream.length) { + if(!this.send_buffer.length) { let reply = this.ipv4_reply(); reply.tcp.fin = true; adapter_receive(this.net, reply); @@ -1162,12 +1160,12 @@ TCPConnection.prototype.close = function() { }; TCPConnection.prototype.pump = function() { - if(this.send_stream.length > 0 && !this.pending) { + if(this.send_buffer.length > 0 && !this.pending) { const data = this.send_chunk_buf; - const n_ready = this.send_stream.peek(data); + const n_ready = this.send_buffer.peek(data); const reply = this.ipv4_reply(); this.pending = true; - if(this.state === TCP_STATE_FIN_WAIT_1 && this.send_stream.length === n_ready) { + if(this.state === TCP_STATE_FIN_WAIT_1 && this.send_buffer.length === n_ready) { reply.tcp.fin = true; } reply.tcp.psh = true; From ac57d576b84966caa758dd11c0d1e4c5df71dddd Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Fri, 8 Nov 2024 21:28:52 +0100 Subject: [PATCH 47/90] remove unused functions --- src/browser/fake_network.js | 46 ------------------------------------ src/browser/fetch_network.js | 6 ----- 2 files changed, 52 deletions(-) diff --git a/src/browser/fake_network.js b/src/browser/fake_network.js index 759e14226b..bf19515b22 100644 --- a/src/browser/fake_network.js +++ b/src/browser/fake_network.js @@ -940,52 +940,6 @@ function write_tcp(spec, out) { return total_length; } -function fake_tcp_connect(dport, adapter) -{ - const vm_ip_str = adapter.vm_ip.join("."); - const router_ip_str = adapter.router_ip.join("."); - const sport_0 = (Math.random() * TCP_DYNAMIC_PORT_RANGE) | 0; - let sport, tuple, sport_i = 0; - do { - sport = TCP_DYNAMIC_PORT_START + ((sport_0 + sport_i) % TCP_DYNAMIC_PORT_RANGE); - tuple = `${vm_ip_str}:${dport}:${router_ip_str}:${sport}`; - } while(++sport_i < TCP_DYNAMIC_PORT_RANGE && adapter.tcp_conn[tuple]); - if(adapter.tcp_conn[tuple]) { - throw new Error("pool of dynamic TCP port numbers exhausted, connection aborted"); - } - - let reader; - let connector; - - let conn = new TCPConnection(); - conn.net = adapter; - conn.on_data = function(data) { if(reader) reader.call(handle, data); }; - conn.on_connect = function() { if(connector) connector.call(handle); }; - conn.tuple = tuple; - - conn.hsrc = adapter.router_mac; - conn.psrc = adapter.router_ip; - conn.sport = sport; - conn.hdest = adapter.vm_mac; - conn.dport = dport; - conn.pdest = adapter.vm_ip; - - adapter.tcp_conn[tuple] = conn; - conn.connect(); - - // TODO: Real event source - let handle = { - write: function(data) { conn.write(data); }, - on: function(event, cb) { - if( event === "data" ) reader = cb; - if( event === "connect" ) connector = cb; - }, - close: function() { conn.close(); } - }; - - return handle; -} - /** * @constructor */ diff --git a/src/browser/fetch_network.js b/src/browser/fetch_network.js index 0a8b5f7fa5..3813f3dd28 100644 --- a/src/browser/fetch_network.js +++ b/src/browser/fetch_network.js @@ -198,12 +198,6 @@ FetchNetworkAdapter.prototype.send = function(data) handle_fake_networking(data, this); }; - -FetchNetworkAdapter.prototype.tcp_connect = function(dport) -{ - return fake_tcp_connect(dport, this); -}; - /** * @param {Uint8Array} data */ From 82ad0426f7cd89c654dd280dde5d9c1e52804df2 Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Sun, 10 Nov 2024 10:24:37 +0100 Subject: [PATCH 48/90] improve TCP state mechanics support For the most part, this patch adds graceful TCP connection shutdown behaviour to class TCPConnection in fake_network.js. Network-intense guest applications like "apt" run noticeably smoother with support for graceful TCP shutdown. Changes in detail: - completed SYN-related state mechanics (TCP connection establishment) - added FIN-related state mechanics (TCP connection shutdown) - reordered and regrouped TCP packet handling steps in TCPConnection.process() - reduced minimum IPv4 packet size from 46 to 40 bytes to reduce noise in logs (why is this even checked) - added explicit detection of TCP Keep-Alive packets to reduce noise in logs - added dbg_log() calls to trace TCP connection state (could be removed if unwanted, or maybe use smth. like LOG_TCP instead of LOG_FETCH?) - removed TCPConnection.seq_history as it was not used anywhere As should be expected, the changes related to TCP connection state only affect class TCPConnection internally and are mostly concentrated in its methods process() and pump(). A few TCP connection states are left unimplemented, reasons: - CLOSING: simultaneous close from both ends (deemed too unlikely) - TIME_WAIT: wait seconds to minutes after sending final ACK before entering CLOSED state (pointless in our specific case and would require a Timer) - LISTEN: are server sockets even supported? Both WISP and FETCH do not support them. Also, TCPConnection.connect() seems not to be called from anywhere (it used to be called from fake_tcp_connect() which was deleted in the previous patcH). As before, fake_network.js does not use any Timers (i.e. setTimeout()), the code is purely driven by outside callers. That means that there are no checks for lost packets (or resends), the internal network is assumed to be ideal (as was the case before). Even though I fixed and removed most of the TODOs I originally found in fake_network.js, I've added several new ones that are left to be discussed. Tested with V86 network providers WispNetworkAdapter and FetchNetworkAdapter, and with V86 network devices ne2k (FreeDOS) and virtio (Debian 12 32-bit). --- src/browser/fake_network.js | 287 +++++++++++++++++++++++++----------- 1 file changed, 200 insertions(+), 87 deletions(-) diff --git a/src/browser/fake_network.js b/src/browser/fake_network.js index bf19515b22..cb7a751a9e 100644 --- a/src/browser/fake_network.js +++ b/src/browser/fake_network.js @@ -17,17 +17,38 @@ const TWO_TO_32 = Math.pow(2, 32); const DHCP_MAGIC_COOKIE = 0x63825363; const V86_ASCII = [118, 56, 54]; -//const TCP_STATE_CLOSED = "closed"; +/* TCP 4-way close handshake + * + * Initiator Receiver + * (active close) (passive close) + * : : + * ESTABLISHED | | ESTABLISHED + * | | + * FIN_WAIT_1 |--- FIN -->| CLOSE_WAIT [1] + * | | + * FIN_WAIT_2 |<-- ACK ---| [2] + * | | + * TIME_WAIT |<-- FIN ---| LAST_ACK [3] + * | | + * |--- ACK -->| CLOSED [4] + * CLOSED | | + * + * - Receiver MAY combine [2] and [3] into a single ACK+FIN packet. + * - State CLOSING (simultaneous close from both ends) not implemented. + * - In [3], instead of TIME_WAIT go diretly to CLOSED after sending ACK. + * - TODO: what about state LISTEN? Neither WISP nor FETCH seem to support it. + */ +const TCP_STATE_CLOSED = "closed"; +const TCP_STATE_SYN_RECEIVED = "syn-received"; +const TCP_STATE_SYN_SENT = "syn-sent"; //const TCP_STATE_LISTEN = "listen"; const TCP_STATE_ESTABLISHED = "established"; const TCP_STATE_FIN_WAIT_1 = "fin-wait-1"; -//const TCP_STATE_CLOSE_WAIT = "close-wait"; -//const TCP_STATE_FIN_WAIT_2 = "fin-wait-2"; -//const TCP_STATE_LAST_ACK = "last-ack"; +const TCP_STATE_CLOSE_WAIT = "close-wait"; +const TCP_STATE_FIN_WAIT_2 = "fin-wait-2"; +const TCP_STATE_LAST_ACK = "last-ack"; //const TCP_STATE_CLOSING = "closing"; //const TCP_STATE_TIME_WAIT = "time-wait"; -const TCP_STATE_SYN_RECEIVED = "syn-received"; -const TCP_STATE_SYN_SENT = "syn-sent"; // source: RFC6335, 6. Port Number Ranges const TCP_DYNAMIC_PORT_START = 49152; @@ -54,7 +75,7 @@ function a2ethaddr(bytes) { return [0,1,2,3,4,5].map((i) => bytes[i].toString(16)).map(x => x.length === 1 ? "0" + x : x).join(":"); } -function siptolong(s) { +function siptolong(s) { // TODO: this function is not used anywhere, can it be removed? let parts = s.split(".").map(function(x) { return parseInt(x, 10); }); return parts[0] << 24 | parts[1] << 16 | parts[2] << 8 | parts[3]; } @@ -211,11 +232,12 @@ function view_setString(offset, str, view, out) function view_setInetChecksum(offset, length, checksum, view, out) { const uint16_end = view.byteOffset + (length & ~1); + const eth_frame = out.eth_frame; for(let i = view.byteOffset; i < uint16_end; i += 2) { - checksum += out.eth_frame[i] << 8 | out.eth_frame[i+1]; + checksum += eth_frame[i] << 8 | eth_frame[i+1]; } if(length & 1) { - checksum += out.eth_frame[uint16_end] << 8; + checksum += eth_frame[uint16_end] << 8; } while(checksum >> 16) { checksum = (checksum & 0xffff) + (checksum >> 16); @@ -248,7 +270,7 @@ function handle_fake_tcp(packet, adapter) if(packet.tcp.syn) { if(adapter.tcp_conn[tuple]) { - dbg_log("SYN to already opened port", LOG_FETCH); + dbg_log("SYN to already opened port", LOG_FETCH); // TODO: is LOG_FETCH a good choice for this module? } if(adapter.on_tcp_connection(adapter, packet, tuple)) { return; @@ -295,7 +317,7 @@ function handle_fake_dns(packet, adapter) let q = packet.dns.questions[i]; switch(q.type){ - case 1: // A recrod + case 1: // A record answers.push({ name: q.name, type: q.type, @@ -458,7 +480,7 @@ function parse_eth(data, o) { o.eth = eth; - // Remove CRC from the end of the packet maybe? + // TODO: Remove CRC from the end of the packet maybe? let payload = data.subarray(ETH_HEADER_SIZE, data.length); if(ethertype === ETHERTYPE_IPV4) { @@ -547,7 +569,12 @@ function parse_ipv4(data, o) { }; // Ethernet minmum packet size. - if(Math.max(len, 46) !== data.length) dbg_log(`ipv4 Length mismatch: ${len} != ${data.length}`, LOG_FETCH); + /* TODO: What's the overall reasoning behind this check, and where does the 46 come from? We get plenty of IP packets of size 40. + if(Math.max(len, 46) !== data.length) { + */ + if(Math.max(len, 40) !== data.length) { + dbg_log(`ipv4 Length mismatch: ${len} != ${data.length}`, LOG_FETCH); + } o.ipv4 = ipv4; let ipdata = data.subarray(ihl * 4, len); @@ -945,9 +972,10 @@ function write_tcp(spec, out) { */ function TCPConnection() { + this.state = TCP_STATE_CLOSED; this.send_buffer = new GrowableRingbuffer(2048, 0); this.send_chunk_buf = new Uint8Array(TCP_PAYLOAD_SIZE); - this.seq_history = []; + this.delayed_active_close = false; } TCPConnection.prototype.ipv4_reply = function() { @@ -969,7 +997,27 @@ TCPConnection.prototype.ipv4_reply = function() { return reply; }; +TCPConnection.prototype.packet_reply = function(packet, tcp_options) { + const reply_tcp = { + sport: packet.tcp.dport, + dport: packet.tcp.sport, + winsize: packet.tcp.winsize, + ackn: this.ack, + seq: this.seq + }; + if(tcp_options) { + for(const opt in tcp_options) { + reply_tcp[opt] = tcp_options[opt]; + } + } + const reply = this.ipv4_reply(); + reply.tcp = reply_tcp; + return reply; +}; + +// TODO: Is this method used anywhere anymore? It used to be called from fake_tcp_connect() which was removed. TCPConnection.prototype.connect = function() { + dbg_log(`TCP[${this.tuple}]: connect(): sending SYN+ACK in state "${this.state}", next "${TCP_STATE_SYN_SENT}"`, LOG_FETCH); this.seq = 1338; this.ack = 1; this.start_seq = 0; @@ -1002,7 +1050,6 @@ TCPConnection.prototype.accept = function(packet) { this.winsize = packet.tcp.winsize; let reply = this.ipv4_reply(); - reply.tcp = { sport: this.sport, dport: this.dport, @@ -1012,86 +1059,128 @@ TCPConnection.prototype.accept = function(packet) { syn: true, ack: true }; + dbg_log(`TCP[${this.tuple}]: accept(): sending SYN+ACK in state "${this.state}", next "${TCP_STATE_SYN_SENT}"`, LOG_FETCH); + this.state = TCP_STATE_SYN_SENT; adapter_receive(this.net, reply); }; TCPConnection.prototype.process = function(packet) { - // Receive Handshake Part 2, Send Part 3 - if(packet.tcp.syn) { - dbg_assert(packet.tcp.ack); - dbg_assert(this.state === TCP_STATE_SYN_SENT); - - this.ack = packet.tcp.seq + 1; - this.start_seq = packet.tcp.seq; - this.last_received_ackn = packet.tcp.ackn; - - let reply = this.ipv4_reply(); - adapter_receive(this.net, reply); - - this.state = TCP_STATE_ESTABLISHED; - if(this.on_connect) this.on_connect.call(this); + if(this.state === TCP_STATE_CLOSED) { + dbg_log(`TCP[${this.tuple}]: WARNING: connection already closed, packet dropped`, LOG_FETCH); + adapter_receive(this.net, this.packet_reply(packet, {rst: true})); return; } - - if(packet.tcp.fin) { - dbg_log(`All done with ${this.tuple} resetting`, LOG_FETCH); - if(this.ack !== packet.tcp.seq) { - dbg_log("Closing the connecton, but seq was wrong", LOG_FETCH); - ++this.ack; // FIN increases seq# - } - let reply = this.ipv4_reply(); - reply.tcp = { - sport: packet.tcp.dport, - dport: packet.tcp.sport, - seq: this.seq, - ackn: this.ack, - winsize: packet.tcp.winsize, - rst: true, - }; - delete this.net.tcp_conn[this.tuple]; - adapter_receive(this.net, reply); + else if(packet.tcp.rst) { + dbg_log(`TCP[${this.tuple}]: received RST in state "${this.state}"`, LOG_FETCH); + this.release(); return; } - - if(this.ack !== packet.tcp.seq) { - dbg_log(`Packet seq was wrong ex: ${this.ack} ~${this.ack - this.start_seq} pk: ${packet.tcp.seq} ~${this.start_seq - packet.tcp.seq} (${this.ack - packet.tcp.seq}) = ${this.name}`, LOG_FETCH); - - let reply = this.ipv4_reply(); - reply.tcp = { - sport: packet.tcp.dport, - dport: packet.tcp.sport, - seq: this.seq, - ackn: this.ack, - winsize: packet.tcp.winsize, - ack: true - }; - adapter_receive(this.net, reply); - + else if(packet.tcp.syn) { + if(this.state === TCP_STATE_SYN_SENT && packet.tcp.ack) { + this.ack = packet.tcp.seq + 1; + this.start_seq = packet.tcp.seq; + this.last_received_ackn = packet.tcp.ackn; + adapter_receive(this.net, this.ipv4_reply()); + dbg_log(`TCP[${this.tuple}]: received SYN+ACK in state "${this.state}", next "${TCP_STATE_ESTABLISHED}"`, LOG_FETCH); + this.state = TCP_STATE_ESTABLISHED; + if(this.on_connect) { + this.on_connect.call(this); + } + } + else { + dbg_log(`TCP[${this.tuple}]: WARNING: unexpected SYN packet dropped`, LOG_FETCH); + } + if(packet.tcp_data.length) { + dbg_log(`TCP[${this.tuple}]: WARNING: ${packet.tcp_data.length} bytes of unexpected SYN packet payload dropped`, LOG_FETCH); + } return; } - this.seq_history.push(`${packet.tcp.seq - this.start_seq}:${packet.tcp.seq + packet.tcp_data.length- this.start_seq}`); - - this.ack += packet.tcp_data.length; - - if(packet.tcp_data.length > 0) { - let reply = this.ipv4_reply(); - adapter_receive(this.net, reply); + if(packet.tcp.ack) { + if(this.state === TCP_STATE_SYN_SENT) { + dbg_log(`TCP[${this.tuple}]: received ACK in state "${this.state}", next "${TCP_STATE_ESTABLISHED}"`, LOG_FETCH); + this.state = TCP_STATE_ESTABLISHED; + } + else if(this.state === TCP_STATE_FIN_WAIT_1) { + dbg_log(`TCP[${this.tuple}]: received ACK in state "${this.state}", next "${TCP_STATE_FIN_WAIT_2}"`, LOG_FETCH); + this.state = TCP_STATE_FIN_WAIT_2; + } + else if(this.state === TCP_STATE_LAST_ACK) { + dbg_log(`TCP[${this.tuple}]: received ACK in state "${this.state}"`, LOG_FETCH); + this.release(); + return; + } } - if(this.last_received_ackn === undefined) this.last_received_ackn = packet.tcp.ackn; - let nread = packet.tcp.ackn - this.last_received_ackn; - //console.log("Read ", nread, "(", this.last_received_ackn, ") ", packet.tcp.ackn, packet.tcp.winsize) - if(nread > 0) { + if(this.last_received_ackn === undefined) { this.last_received_ackn = packet.tcp.ackn; - this.send_buffer.remove(nread); - this.seq += nread; - this.pending = false; + } + else { + const n_ack = packet.tcp.ackn - this.last_received_ackn; + //console.log("Read ", n_ack, "(", this.last_received_ackn, ") ", packet.tcp.ackn, packet.tcp.winsize) + if(n_ack > 0) { + this.last_received_ackn = packet.tcp.ackn; + this.send_buffer.remove(n_ack); + this.seq += n_ack; + this.pending = false; + } + else if(n_ack < 0) { // TODO: any better way to handle this? could this just be a 32-bit sequence number overflow? + dbg_log(`TCP[${this.tuple}]: ERROR: ack underflow (pkt=${packet.tcp.ackn} last=${this.last_received_ackn}), resetting`, LOG_FETCH); + adapter_receive(this.net, this.packet_reply(packet, {rst: true})); + this.release(); + return; + } } - if(nread < 0) return; + if(packet.tcp.fin) { + dbg_log(`TCP[${this.tuple}]: received FIN in state "${this.state}", payload: ${packet.tcp_data.length}`, LOG_FETCH); + if(this.ack !== packet.tcp.seq) { + dbg_log(`TCP[${this.tuple}]: WARNING: closing connection in state "${this.state}" with invalid seq (${this.ack} != ${packet.tcp.seq})`, LOG_FETCH); + } + ++this.ack; // FIN increases seqnr + const reply = this.packet_reply(packet, {}); + if(this.state === TCP_STATE_ESTABLISHED) { + reply.tcp.ack = true; + if(this.send_buffer.length) { + this.state = TCP_STATE_CLOSE_WAIT; + } + else { + dbg_log(`TCP[${this.tuple}]: sending FIN in state "${this.state}", next "${TCP_STATE_LAST_ACK}"`, LOG_FETCH); + this.state = TCP_STATE_LAST_ACK; + reply.tcp.fin = true; + } + } + else if(this.state === TCP_STATE_FIN_WAIT_2) { + this.release(); + reply.tcp.ack = true; + } + else { + dbg_log(`TCP[${this.tuple}]: ERROR: received FIN in unexpected TCP state "${this.state}", resetting`, LOG_FETCH); + this.release(); + reply.tcp.rst = true; + } + adapter_receive(this.net, reply); + } + else if(this.ack !== packet.tcp.seq) { + // Handle TCP Keep-Alives silently. + // Excerpt from RFC 9293, 3.8.4. TCP Keep-Alives: + // To confirm that an idle connection is still active, these + // implementations send a probe segment designed to elicit a response + // from the TCP peer. Such a segment generally contains SEG.SEQ = + // SND.NXT-1 and may or may not contain one garbage octet of data. + if(this.ack !== packet.tcp.seq + 1) { + dbg_log(`Packet seq was wrong ex: ${this.ack} ~${this.ack - this.start_seq} ` + + `pk: ${packet.tcp.seq} ~${this.start_seq - packet.tcp.seq} ` + + `(${this.ack - packet.tcp.seq}) = ${this.name}`, LOG_FETCH); + } + adapter_receive(this.net, this.packet_reply(packet, {ack: true})); + } + else if(packet.tcp.ack && packet.tcp_data.length > 0) { + this.ack += packet.tcp_data.length; + adapter_receive(this.net, this.ipv4_reply()); + this.on_data(packet.tcp_data); + } - this.on_data(packet.tcp_data); this.pump(); }; @@ -1104,27 +1193,51 @@ TCPConnection.prototype.write = function(data) { }; TCPConnection.prototype.close = function() { - this.state = TCP_STATE_FIN_WAIT_1; - if(!this.send_buffer.length) { - let reply = this.ipv4_reply(); - reply.tcp.fin = true; - adapter_receive(this.net, reply); + if(this.state === TCP_STATE_ESTABLISHED && !this.delayed_active_close) { + if(this.send_buffer.length) { + dbg_log(`TCP[${this.tuple}]: active close, delaying FIN in state "${this.state}"`, LOG_FETCH); + this.delayed_active_close = true; + } + else { + dbg_log(`TCP[${this.tuple}]: active close, sending FIN in state "${this.state}", next "${TCP_STATE_FIN_WAIT_1}"`, LOG_FETCH); + this.state = TCP_STATE_FIN_WAIT_1; + let fin_reply = this.ipv4_reply(); + fin_reply.tcp.fin = true; + adapter_receive(this.net, fin_reply); + } } this.pump(); }; +TCPConnection.prototype.release = function() { + if(this.net.tcp_conn[this.tuple]) { + dbg_log(`TCP[${this.tuple}]: connection closed in state "${this.state}"`, LOG_FETCH); + this.state = TCP_STATE_CLOSED; + delete this.net.tcp_conn[this.tuple]; + } +}; + TCPConnection.prototype.pump = function() { if(this.send_buffer.length > 0 && !this.pending) { const data = this.send_chunk_buf; const n_ready = this.send_buffer.peek(data); const reply = this.ipv4_reply(); - this.pending = true; - if(this.state === TCP_STATE_FIN_WAIT_1 && this.send_buffer.length === n_ready) { - reply.tcp.fin = true; - } reply.tcp.psh = true; reply.tcp_data = data.subarray(0, n_ready); + if(this.delayed_active_close && this.send_buffer.length === n_ready) { + if(this.state === TCP_STATE_ESTABLISHED) { + dbg_log(`TCP[${this.tuple}]: sending delayed FIN from active close in state "${this.state}", next "${TCP_STATE_FIN_WAIT_1}"`, LOG_FETCH); + this.state = TCP_STATE_FIN_WAIT_1; + reply.tcp.fin = true; + } + else if(this.state === TCP_STATE_CLOSE_WAIT) { + dbg_log(`TCP[${this.tuple}]: sending delayed FIN from active close in state "${this.state}", next "${TCP_STATE_LAST_ACK}"`, LOG_FETCH); + this.state = TCP_STATE_LAST_ACK; + reply.tcp.fin = true; + } + } adapter_receive(this.net, reply); + this.pending = true; } }; From 5b7aee7f72f76e04cafefff1b3a45ce280852cd0 Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Mon, 11 Nov 2024 21:28:24 +0100 Subject: [PATCH 49/90] suggested improvements and a bugfix - removed orphaned function siptolong() - refactored view_setInetChecksum() to calc_inet_checksum() - renamed view_setString() into view_set_string() and simplified it - renamed view_setArray() into view_set_array() - refactored adapter_receive() to make_packet() - bugfix: send FIN after receiving last ACK instead of tagging it to the last packet we send, fixes a corner case when TCPConnection.close() is called with empty send_buffer but with a yet to be acknowledged package in transit. Send a FIN packet in TCPConnection.close() only if both the send_buffer is empty and there is no package in transit, else delay that until we receive the last ACK. --- src/browser/fake_network.js | 151 +++++++++++++++++++----------------- src/browser/wisp_network.js | 4 +- 2 files changed, 80 insertions(+), 75 deletions(-) diff --git a/src/browser/fake_network.js b/src/browser/fake_network.js index cb7a751a9e..45eb4f947a 100644 --- a/src/browser/fake_network.js +++ b/src/browser/fake_network.js @@ -75,11 +75,6 @@ function a2ethaddr(bytes) { return [0,1,2,3,4,5].map((i) => bytes[i].toString(16)).map(x => x.length === 1 ? "0" + x : x).join(":"); } -function siptolong(s) { // TODO: this function is not used anywhere, can it be removed? - let parts = s.split(".").map(function(x) { return parseInt(x, 10); }); - return parts[0] << 24 | parts[1] << 16 | parts[2] << 8 | parts[3]; -} - function iptolong(parts) { return parts[0] << 24 | parts[1] << 16 | parts[2] << 8 | parts[3]; } @@ -198,7 +193,7 @@ function create_eth_encoder_buf() * @param {DataView} view * @param {Object} out */ -function view_setArray(offset, data, view, out) +function view_set_array(offset, data, view, out) { out.eth_frame.set(data, view.byteOffset + offset); return data.length; @@ -212,24 +207,21 @@ function view_setArray(offset, data, view, out) * @param {DataView} view * @param {Object} out */ -function view_setString(offset, str, view, out) +function view_set_string(offset, str, view, out) { - const ofs = view.byteOffset + offset; - const result = out.text_encoder.encodeInto(str, ofs ? out.eth_frame.subarray(ofs) : out.eth_frame); - return result.written; + return out.text_encoder.encodeInto(str, out.eth_frame.subarray(view.byteOffset + offset)).written; } /** - * Calculate internet checksum for view[0 : length], store 16-bit result in view at offset. + * Calculate internet checksum for view[0 : length] and return the 16-bit result. * Source: RFC768 and RFC1071 (chapter 4.1). * - * @param {number} offset * @param {number} length * @param {number} checksum * @param {DataView} view * @param {Object} out */ -function view_setInetChecksum(offset, length, checksum, view, out) +function calc_inet_checksum(length, checksum, view, out) { const uint16_end = view.byteOffset + (length & ~1); const eth_frame = out.eth_frame; @@ -242,19 +234,18 @@ function view_setInetChecksum(offset, length, checksum, view, out) while(checksum >> 16) { checksum = (checksum & 0xffff) + (checksum >> 16); } - view.setUint16(offset, ~checksum); + return ~checksum & 0xffff; } /** - * @param {WispNetworkAdapter|FetchNetworkAdapter} adapter + * @param {Object} out * @param {Object} spec */ -function adapter_receive(adapter, spec) +function make_packet(out, spec) { dbg_assert(spec.eth); - const out = adapter.eth_encoder_buf; out.eth_frame.fill(0); - adapter.receive(out.eth_frame.subarray(0, write_eth(spec, out))); + return out.eth_frame.subarray(0, write_eth(spec, out)); } function handle_fake_tcp(packet, adapter) @@ -290,7 +281,7 @@ function handle_fake_tcp(packet, adapter) rst: true, ack: packet.tcp.syn }; - adapter_receive(adapter, reply); + adapter.receive(make_packet(adapter.eth_encoder_buf, reply)); return true; } @@ -336,7 +327,7 @@ function handle_fake_dns(packet, adapter) questions: packet.dns.questions, answers: answers }; - adapter_receive(adapter, reply); + adapter.receive(make_packet(adapter.eth_encoder_buf, reply)); return true; } @@ -367,7 +358,7 @@ function handle_fake_ntp(packet, adapter) { reply.ntp.trans_ts_f = now_n_f; reply.ntp.stratum = 2; - adapter_receive(adapter, reply); + adapter.receive(make_packet(adapter.eth_encoder_buf, reply)); return true; } @@ -422,7 +413,7 @@ function handle_fake_dhcp(packet, adapter) { options.push(new Uint8Array([255, 0])); reply.dhcp.options = options; - adapter_receive(adapter, reply); + adapter.receive(make_packet(adapter.eth_encoder_buf, reply)); } function handle_fake_networking(data, adapter) { @@ -499,8 +490,8 @@ function parse_eth(data, o) { function write_eth(spec, out) { const view = out.eth_frame_view; - view_setArray(0, spec.eth.dest, view, out); - view_setArray(6, spec.eth.src, view, out); + view_set_array(0, spec.eth.dest, view, out); + view_set_array(6, spec.eth.src, view, out); view.setUint16(12, spec.eth.ethertype); let len = ETH_HEADER_SIZE; if(spec.arp) { @@ -536,10 +527,10 @@ function write_arp(spec, out) { view.setUint8(4, spec.arp.sha.length); view.setUint8(5, spec.arp.spa.length); view.setUint16(6, spec.arp.oper); - view_setArray(8, spec.arp.sha, view, out); - view_setArray(14, spec.arp.spa, view, out); - view_setArray(18, spec.arp.tha, view, out); - view_setArray(24, spec.arp.tpa, view, out); + view_set_array(8, spec.arp.sha, view, out); + view_set_array(14, spec.arp.spa, view, out); + view_set_array(18, spec.arp.tha, view, out); + view_set_array(24, spec.arp.tpa, view, out); return 28; } @@ -569,7 +560,7 @@ function parse_ipv4(data, o) { }; // Ethernet minmum packet size. - /* TODO: What's the overall reasoning behind this check, and where does the 46 come from? We get plenty of IP packets of size 40. + /* TODO: What's the overall reasoning behind this check, and where does the 46 come from? We get plenty of IP packets of size 40, 42, etc. if(Math.max(len, 46) !== data.length) { */ if(Math.max(len, 40) !== data.length) { @@ -613,9 +604,9 @@ function write_ipv4(spec, out) { view.setUint8(8, spec.ipv4.ttl || 32); view.setUint8(9, spec.ipv4.proto); view.setUint16(10, 0); // checksum initially zero before calculation - view_setArray(12, spec.ipv4.src, view, out); - view_setArray(16, spec.ipv4.dest, view, out); - view_setInetChecksum(10, IPV4_HEADER_SIZE, 0, view, out); + view_set_array(12, spec.ipv4.src, view, out); + view_set_array(16, spec.ipv4.dest, view, out); + view.setUint16(10, calc_inet_checksum(IPV4_HEADER_SIZE, 0, view, out)); return len; } @@ -635,9 +626,9 @@ function write_icmp(spec, out) { view.setUint8(0, spec.icmp.type); view.setUint8(1, spec.icmp.code); view.setUint16(2, 0); // checksum initially zero before calculation - const data_length = view_setArray(ICMP_HEADER_SIZE, spec.icmp.data, view, out); + const data_length = view_set_array(ICMP_HEADER_SIZE, spec.icmp.data, view, out); const total_length = ICMP_HEADER_SIZE + data_length; - view_setInetChecksum(2, total_length, 0, view, out); + view.setUint16(2, calc_inet_checksum(total_length, 0, view, out)); return total_length; } @@ -678,7 +669,7 @@ function write_udp(spec, out) { total_length += write_ntp(spec, out); } else { - total_length += view_setArray(0, spec.udp.data, out.udp_payload_view, out); + total_length += view_set_array(0, spec.udp.data, out.udp_payload_view, out); } view.setUint16(0, spec.udp.sport); @@ -693,7 +684,7 @@ function write_udp(spec, out) { (spec.ipv4.dest[2] << 8 | spec.ipv4.dest[3]) + IPV4_PROTO_UDP + total_length; - view_setInetChecksum(6, total_length, pseudo_header, view, out); + view.setUint16(6, calc_inet_checksum(total_length, pseudo_header, view, out)); return total_length; } @@ -759,7 +750,7 @@ function write_dns(spec, out) { for(let i = 0; i < spec.dns.questions.length; ++i) { let q = spec.dns.questions[i]; for(let s of q.name) { - const n_written = view_setString(offset + 1, s, view, out); + const n_written = view_set_string(offset + 1, s, view, out); view.setUint8(offset, n_written); offset += 1 + n_written; } @@ -771,7 +762,7 @@ function write_dns(spec, out) { function write_reply(a) { for(let s of a.name) { - const n_written = view_setString(offset + 1, s, view, out); + const n_written = view_set_string(offset + 1, s, view, out); view.setUint8(offset, n_written); offset += 1 + n_written; } @@ -783,7 +774,7 @@ function write_dns(spec, out) { offset += 4; view.setUint16(offset, a.data.length); offset += 2; - offset += view_setArray(offset, a.data, view, out); + offset += view_set_array(offset, a.data, view, out); } for(let i = 0; i < spec.dns.answers.length; ++i) { @@ -842,13 +833,13 @@ function write_dhcp(spec, out) { view.setUint32(16, spec.dhcp.yiaddr); view.setUint32(20, spec.dhcp.siaddr); view.setUint32(24, spec.dhcp.giaddr); - view_setArray(28, spec.dhcp.chaddr, view, out); + view_set_array(28, spec.dhcp.chaddr, view, out); view.setUint32(236, DHCP_MAGIC_COOKIE); let offset = 240; for(let o of spec.dhcp.options) { - offset += view_setArray(offset, o, view, out); + offset += view_set_array(offset, o, view, out); } return offset; } @@ -953,7 +944,7 @@ function write_tcp(spec, out) { let total_length = TCP_HEADER_SIZE; if(spec.tcp_data) { - total_length += view_setArray(TCP_HEADER_SIZE, spec.tcp_data, view, out); + total_length += view_set_array(TCP_HEADER_SIZE, spec.tcp_data, view, out); } const pseudo_header = @@ -963,7 +954,7 @@ function write_tcp(spec, out) { (spec.ipv4.dest[2] << 8 | spec.ipv4.dest[3]) + IPV4_PROTO_TCP + total_length; - view_setInetChecksum(16, total_length, pseudo_header, view, out); + view.setUint16(16, calc_inet_checksum(total_length, pseudo_header, view, out)); return total_length; } @@ -1034,7 +1025,7 @@ TCPConnection.prototype.connect = function() { winsize: 0, syn: true, }; - adapter_receive(this.net, reply); + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); }; TCPConnection.prototype.accept = function(packet) { @@ -1061,13 +1052,14 @@ TCPConnection.prototype.accept = function(packet) { }; dbg_log(`TCP[${this.tuple}]: accept(): sending SYN+ACK in state "${this.state}", next "${TCP_STATE_SYN_SENT}"`, LOG_FETCH); this.state = TCP_STATE_SYN_SENT; - adapter_receive(this.net, reply); + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); }; TCPConnection.prototype.process = function(packet) { if(this.state === TCP_STATE_CLOSED) { dbg_log(`TCP[${this.tuple}]: WARNING: connection already closed, packet dropped`, LOG_FETCH); - adapter_receive(this.net, this.packet_reply(packet, {rst: true})); + const reply = this.packet_reply(packet, {rst: true}); + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); return; } else if(packet.tcp.rst) { @@ -1080,7 +1072,9 @@ TCPConnection.prototype.process = function(packet) { this.ack = packet.tcp.seq + 1; this.start_seq = packet.tcp.seq; this.last_received_ackn = packet.tcp.ackn; - adapter_receive(this.net, this.ipv4_reply()); + + const reply = this.ipv4_reply(); + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); dbg_log(`TCP[${this.tuple}]: received SYN+ACK in state "${this.state}", next "${TCP_STATE_ESTABLISHED}"`, LOG_FETCH); this.state = TCP_STATE_ESTABLISHED; if(this.on_connect) { @@ -1123,10 +1117,31 @@ TCPConnection.prototype.process = function(packet) { this.send_buffer.remove(n_ack); this.seq += n_ack; this.pending = false; + + let send_fin; + if(this.delayed_active_close && !this.send_buffer.length) { + if(this.state === TCP_STATE_ESTABLISHED) { + dbg_log(`TCP[${this.tuple}]: sending delayed FIN from active close in state "${this.state}", next "${TCP_STATE_FIN_WAIT_1}"`, LOG_FETCH); + this.state = TCP_STATE_FIN_WAIT_1; + send_fin = true; + } + else if(this.state === TCP_STATE_CLOSE_WAIT) { + dbg_log(`TCP[${this.tuple}]: sending delayed FIN from passive close in state "${this.state}", next "${TCP_STATE_LAST_ACK}"`, LOG_FETCH); + this.state = TCP_STATE_LAST_ACK; + send_fin = true; + } + } + if(send_fin) { + const reply = this.ipv4_reply(); + reply.tcp.fin = true; + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); + return; + } } else if(n_ack < 0) { // TODO: any better way to handle this? could this just be a 32-bit sequence number overflow? dbg_log(`TCP[${this.tuple}]: ERROR: ack underflow (pkt=${packet.tcp.ackn} last=${this.last_received_ackn}), resetting`, LOG_FETCH); - adapter_receive(this.net, this.packet_reply(packet, {rst: true})); + const reply = this.packet_reply(packet, {rst: true}); + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); this.release(); return; } @@ -1141,7 +1156,7 @@ TCPConnection.prototype.process = function(packet) { const reply = this.packet_reply(packet, {}); if(this.state === TCP_STATE_ESTABLISHED) { reply.tcp.ack = true; - if(this.send_buffer.length) { + if(this.send_buffer.length || this.pending) { this.state = TCP_STATE_CLOSE_WAIT; } else { @@ -1159,7 +1174,7 @@ TCPConnection.prototype.process = function(packet) { this.release(); reply.tcp.rst = true; } - adapter_receive(this.net, reply); + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); } else if(this.ack !== packet.tcp.seq) { // Handle TCP Keep-Alives silently. @@ -1173,11 +1188,13 @@ TCPConnection.prototype.process = function(packet) { `pk: ${packet.tcp.seq} ~${this.start_seq - packet.tcp.seq} ` + `(${this.ack - packet.tcp.seq}) = ${this.name}`, LOG_FETCH); } - adapter_receive(this.net, this.packet_reply(packet, {ack: true})); + const reply = this.packet_reply(packet, {ack: true}); + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); } else if(packet.tcp.ack && packet.tcp_data.length > 0) { this.ack += packet.tcp_data.length; - adapter_receive(this.net, this.ipv4_reply()); + const reply = this.ipv4_reply(); + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); this.on_data(packet.tcp_data); } @@ -1194,16 +1211,16 @@ TCPConnection.prototype.write = function(data) { TCPConnection.prototype.close = function() { if(this.state === TCP_STATE_ESTABLISHED && !this.delayed_active_close) { - if(this.send_buffer.length) { + if(this.send_buffer.length || this.pending) { dbg_log(`TCP[${this.tuple}]: active close, delaying FIN in state "${this.state}"`, LOG_FETCH); this.delayed_active_close = true; } else { dbg_log(`TCP[${this.tuple}]: active close, sending FIN in state "${this.state}", next "${TCP_STATE_FIN_WAIT_1}"`, LOG_FETCH); this.state = TCP_STATE_FIN_WAIT_1; - let fin_reply = this.ipv4_reply(); - fin_reply.tcp.fin = true; - adapter_receive(this.net, fin_reply); + const reply = this.ipv4_reply(); + reply.tcp.fin = true; + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); } } this.pump(); @@ -1218,25 +1235,13 @@ TCPConnection.prototype.release = function() { }; TCPConnection.prototype.pump = function() { - if(this.send_buffer.length > 0 && !this.pending) { + if(this.send_buffer.length && !this.pending) { const data = this.send_chunk_buf; const n_ready = this.send_buffer.peek(data); const reply = this.ipv4_reply(); reply.tcp.psh = true; reply.tcp_data = data.subarray(0, n_ready); - if(this.delayed_active_close && this.send_buffer.length === n_ready) { - if(this.state === TCP_STATE_ESTABLISHED) { - dbg_log(`TCP[${this.tuple}]: sending delayed FIN from active close in state "${this.state}", next "${TCP_STATE_FIN_WAIT_1}"`, LOG_FETCH); - this.state = TCP_STATE_FIN_WAIT_1; - reply.tcp.fin = true; - } - else if(this.state === TCP_STATE_CLOSE_WAIT) { - dbg_log(`TCP[${this.tuple}]: sending delayed FIN from active close in state "${this.state}", next "${TCP_STATE_LAST_ACK}"`, LOG_FETCH); - this.state = TCP_STATE_LAST_ACK; - reply.tcp.fin = true; - } - } - adapter_receive(this.net, reply); + this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); this.pending = true; } }; @@ -1269,7 +1274,7 @@ function arp_whohas(packet, adapter) { tha: packet.eth.src, tpa: packet.arp.spa }; - adapter_receive(adapter, reply); + adapter.receive(make_packet(adapter.eth_encoder_buf, reply)); } function handle_fake_ping(packet, adapter) { @@ -1285,7 +1290,7 @@ function handle_fake_ping(packet, adapter) { code: packet.icmp.code, data: packet.icmp.data }; - adapter_receive(adapter, reply); + adapter.receive(make_packet(adapter.eth_encoder_buf, reply)); } function handle_udp_echo(packet, adapter) { @@ -1302,5 +1307,5 @@ function handle_udp_echo(packet, adapter) { dport: packet.udp.sport, data: new TextEncoder().encode(packet.udp.data_s) }; - adapter_receive(adapter, reply); + adapter.receive(make_packet(adapter.eth_encoder_buf, reply)); } diff --git a/src/browser/wisp_network.js b/src/browser/wisp_network.js index 4ecd8c2caa..5b62dc03f9 100644 --- a/src/browser/wisp_network.js +++ b/src/browser/wisp_network.js @@ -249,7 +249,7 @@ WispNetworkAdapter.prototype.send = function(data) rst: true, ack: packet.tcp.syn }; - adapter_receive(this, reply); + this.receive(make_packet(this.eth_encoder_buf, reply)); return; } @@ -273,7 +273,7 @@ WispNetworkAdapter.prototype.send = function(data) reply.udp = { sport: 53, dport: packet.udp.sport }; const result = await ((await fetch(`https://${this.doh_server}/dns-query`, {method: "POST", headers: [["content-type", "application/dns-message"]], body: packet.udp.data})).arrayBuffer()); reply.udp.data = new Uint8Array(result); - adapter_receive(this, reply); + this.receive(make_packet(this.eth_encoder_buf, reply)); })(); } From 253262467eff5c22a2a5aef77dc32eaf368c0e86 Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Mon, 11 Nov 2024 22:47:46 +0100 Subject: [PATCH 50/90] bugfix: late FIN support in case of passive TCP close was incomplete --- src/browser/fake_network.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/browser/fake_network.js b/src/browser/fake_network.js index 45eb4f947a..10301740b8 100644 --- a/src/browser/fake_network.js +++ b/src/browser/fake_network.js @@ -966,7 +966,7 @@ function TCPConnection() this.state = TCP_STATE_CLOSED; this.send_buffer = new GrowableRingbuffer(2048, 0); this.send_chunk_buf = new Uint8Array(TCP_PAYLOAD_SIZE); - this.delayed_active_close = false; + this.delayed_send_fin = false; } TCPConnection.prototype.ipv4_reply = function() { @@ -1119,7 +1119,7 @@ TCPConnection.prototype.process = function(packet) { this.pending = false; let send_fin; - if(this.delayed_active_close && !this.send_buffer.length) { + if(this.delayed_send_fin && !this.send_buffer.length) { if(this.state === TCP_STATE_ESTABLISHED) { dbg_log(`TCP[${this.tuple}]: sending delayed FIN from active close in state "${this.state}", next "${TCP_STATE_FIN_WAIT_1}"`, LOG_FETCH); this.state = TCP_STATE_FIN_WAIT_1; @@ -1158,6 +1158,7 @@ TCPConnection.prototype.process = function(packet) { reply.tcp.ack = true; if(this.send_buffer.length || this.pending) { this.state = TCP_STATE_CLOSE_WAIT; + this.delayed_send_fin = true; } else { dbg_log(`TCP[${this.tuple}]: sending FIN in state "${this.state}", next "${TCP_STATE_LAST_ACK}"`, LOG_FETCH); @@ -1210,10 +1211,10 @@ TCPConnection.prototype.write = function(data) { }; TCPConnection.prototype.close = function() { - if(this.state === TCP_STATE_ESTABLISHED && !this.delayed_active_close) { + if(this.state === TCP_STATE_ESTABLISHED && !this.delayed_send_fin) { if(this.send_buffer.length || this.pending) { dbg_log(`TCP[${this.tuple}]: active close, delaying FIN in state "${this.state}"`, LOG_FETCH); - this.delayed_active_close = true; + this.delayed_send_fin = true; } else { dbg_log(`TCP[${this.tuple}]: active close, sending FIN in state "${this.state}", next "${TCP_STATE_FIN_WAIT_1}"`, LOG_FETCH); From f8b563cb91f3db80d5dd335717f733c83312bc5a Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Sun, 17 Nov 2024 16:48:10 +0100 Subject: [PATCH 51/90] added more TCP state mechanics See https://en.wikipedia.org/wiki/File:Tcp_state_diagram_fixed_new.svg for a full TCP state diagram, all states (except TIME_WAIT) and state transitions below ESTABLISHED are now properly implemented in class TCPConnection. - fixed misconceptions regarding TCP close states and transitions - fixed incorrect behaviour when guest closes a TCP connection - added state CLOSING to state machine - added support to pass passive close up to WispNetworkAdapter - commented out all calls to dbg_log() that are part of this PR This improves interoperability by further completing the TCP state machine model in fake_network.js. --- src/browser/fake_network.js | 136 ++++++++++++++++++++---------------- 1 file changed, 75 insertions(+), 61 deletions(-) diff --git a/src/browser/fake_network.js b/src/browser/fake_network.js index 10301740b8..827a578aa2 100644 --- a/src/browser/fake_network.js +++ b/src/browser/fake_network.js @@ -17,26 +17,14 @@ const TWO_TO_32 = Math.pow(2, 32); const DHCP_MAGIC_COOKIE = 0x63825363; const V86_ASCII = [118, 56, 54]; -/* TCP 4-way close handshake +/* For the complete TCP state diagram see: * - * Initiator Receiver - * (active close) (passive close) - * : : - * ESTABLISHED | | ESTABLISHED - * | | - * FIN_WAIT_1 |--- FIN -->| CLOSE_WAIT [1] - * | | - * FIN_WAIT_2 |<-- ACK ---| [2] - * | | - * TIME_WAIT |<-- FIN ---| LAST_ACK [3] - * | | - * |--- ACK -->| CLOSED [4] - * CLOSED | | + * https://en.wikipedia.org/wiki/File:Tcp_state_diagram_fixed_new.svg * - * - Receiver MAY combine [2] and [3] into a single ACK+FIN packet. - * - State CLOSING (simultaneous close from both ends) not implemented. - * - In [3], instead of TIME_WAIT go diretly to CLOSED after sending ACK. - * - TODO: what about state LISTEN? Neither WISP nor FETCH seem to support it. + * State TIME_WAIT is not needed, we can skip it and transition directly to CLOSED instead. + * + * TODO: + * - What about state LISTEN? Neither WISP nor FETCH seem to support it. */ const TCP_STATE_CLOSED = "closed"; const TCP_STATE_SYN_RECEIVED = "syn-received"; @@ -47,7 +35,7 @@ const TCP_STATE_FIN_WAIT_1 = "fin-wait-1"; const TCP_STATE_CLOSE_WAIT = "close-wait"; const TCP_STATE_FIN_WAIT_2 = "fin-wait-2"; const TCP_STATE_LAST_ACK = "last-ack"; -//const TCP_STATE_CLOSING = "closing"; +const TCP_STATE_CLOSING = "closing"; //const TCP_STATE_TIME_WAIT = "time-wait"; // source: RFC6335, 6. Port Number Ranges @@ -966,7 +954,9 @@ function TCPConnection() this.state = TCP_STATE_CLOSED; this.send_buffer = new GrowableRingbuffer(2048, 0); this.send_chunk_buf = new Uint8Array(TCP_PAYLOAD_SIZE); + this.in_active_close = false; this.delayed_send_fin = false; + this.delayed_state = undefined; } TCPConnection.prototype.ipv4_reply = function() { @@ -1006,9 +996,10 @@ TCPConnection.prototype.packet_reply = function(packet, tcp_options) { return reply; }; +/* // TODO: Is this method used anywhere anymore? It used to be called from fake_tcp_connect() which was removed. TCPConnection.prototype.connect = function() { - dbg_log(`TCP[${this.tuple}]: connect(): sending SYN+ACK in state "${this.state}", next "${TCP_STATE_SYN_SENT}"`, LOG_FETCH); + // dbg_log(`TCP[${this.tuple}]: connect(): sending SYN+ACK in state "${this.state}", next "${TCP_STATE_SYN_SENT}"`, LOG_FETCH); this.seq = 1338; this.ack = 1; this.start_seq = 0; @@ -1027,6 +1018,7 @@ TCPConnection.prototype.connect = function() { }; this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); }; +*/ TCPConnection.prototype.accept = function(packet) { this.seq = 1338; @@ -1050,20 +1042,20 @@ TCPConnection.prototype.accept = function(packet) { syn: true, ack: true }; - dbg_log(`TCP[${this.tuple}]: accept(): sending SYN+ACK in state "${this.state}", next "${TCP_STATE_SYN_SENT}"`, LOG_FETCH); - this.state = TCP_STATE_SYN_SENT; + // dbg_log(`TCP[${this.tuple}]: accept(): sending SYN+ACK in state "${this.state}", next "${TCP_STATE_ESTABLISHED}"`, LOG_FETCH); + this.state = TCP_STATE_ESTABLISHED; this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); }; TCPConnection.prototype.process = function(packet) { if(this.state === TCP_STATE_CLOSED) { - dbg_log(`TCP[${this.tuple}]: WARNING: connection already closed, packet dropped`, LOG_FETCH); + // dbg_log(`TCP[${this.tuple}]: WARNING: connection already closed, packet dropped`, LOG_FETCH); const reply = this.packet_reply(packet, {rst: true}); this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); return; } else if(packet.tcp.rst) { - dbg_log(`TCP[${this.tuple}]: received RST in state "${this.state}"`, LOG_FETCH); + // dbg_log(`TCP[${this.tuple}]: received RST in state "${this.state}"`, LOG_FETCH); this.release(); return; } @@ -1075,7 +1067,7 @@ TCPConnection.prototype.process = function(packet) { const reply = this.ipv4_reply(); this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); - dbg_log(`TCP[${this.tuple}]: received SYN+ACK in state "${this.state}", next "${TCP_STATE_ESTABLISHED}"`, LOG_FETCH); + // dbg_log(`TCP[${this.tuple}]: received SYN+ACK in state "${this.state}", next "${TCP_STATE_ESTABLISHED}"`, LOG_FETCH); this.state = TCP_STATE_ESTABLISHED; if(this.on_connect) { this.on_connect.call(this); @@ -1091,16 +1083,14 @@ TCPConnection.prototype.process = function(packet) { } if(packet.tcp.ack) { - if(this.state === TCP_STATE_SYN_SENT) { - dbg_log(`TCP[${this.tuple}]: received ACK in state "${this.state}", next "${TCP_STATE_ESTABLISHED}"`, LOG_FETCH); - this.state = TCP_STATE_ESTABLISHED; - } - else if(this.state === TCP_STATE_FIN_WAIT_1) { - dbg_log(`TCP[${this.tuple}]: received ACK in state "${this.state}", next "${TCP_STATE_FIN_WAIT_2}"`, LOG_FETCH); - this.state = TCP_STATE_FIN_WAIT_2; + if(this.state === TCP_STATE_FIN_WAIT_1) { + if(!packet.tcp.fin) { // handle FIN+ACK in FIN_WAIT_1 separately further down below + // dbg_log(`TCP[${this.tuple}]: received ACK in state "${this.state}", next "${TCP_STATE_FIN_WAIT_2}"`, LOG_FETCH); + this.state = TCP_STATE_FIN_WAIT_2; + } } - else if(this.state === TCP_STATE_LAST_ACK) { - dbg_log(`TCP[${this.tuple}]: received ACK in state "${this.state}"`, LOG_FETCH); + else if(this.state === TCP_STATE_CLOSING || this.state === TCP_STATE_LAST_ACK) { + // dbg_log(`TCP[${this.tuple}]: received ACK in state "${this.state}"`, LOG_FETCH); this.release(); return; } @@ -1118,20 +1108,10 @@ TCPConnection.prototype.process = function(packet) { this.seq += n_ack; this.pending = false; - let send_fin; if(this.delayed_send_fin && !this.send_buffer.length) { - if(this.state === TCP_STATE_ESTABLISHED) { - dbg_log(`TCP[${this.tuple}]: sending delayed FIN from active close in state "${this.state}", next "${TCP_STATE_FIN_WAIT_1}"`, LOG_FETCH); - this.state = TCP_STATE_FIN_WAIT_1; - send_fin = true; - } - else if(this.state === TCP_STATE_CLOSE_WAIT) { - dbg_log(`TCP[${this.tuple}]: sending delayed FIN from passive close in state "${this.state}", next "${TCP_STATE_LAST_ACK}"`, LOG_FETCH); - this.state = TCP_STATE_LAST_ACK; - send_fin = true; - } - } - if(send_fin) { + // dbg_log(`TCP[${this.tuple}]: sending delayed FIN from active close in state "${this.state}", next "${this.delayed_state}"`, LOG_FETCH); + this.delayed_send_fin = false; + this.state = this.delayed_state; const reply = this.ipv4_reply(); reply.tcp.fin = true; this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); @@ -1148,30 +1128,45 @@ TCPConnection.prototype.process = function(packet) { } if(packet.tcp.fin) { - dbg_log(`TCP[${this.tuple}]: received FIN in state "${this.state}", payload: ${packet.tcp_data.length}`, LOG_FETCH); if(this.ack !== packet.tcp.seq) { dbg_log(`TCP[${this.tuple}]: WARNING: closing connection in state "${this.state}" with invalid seq (${this.ack} != ${packet.tcp.seq})`, LOG_FETCH); } ++this.ack; // FIN increases seqnr const reply = this.packet_reply(packet, {}); if(this.state === TCP_STATE_ESTABLISHED) { + // dbg_log(`TCP[${this.tuple}]: received FIN in state "${this.state}, next "${TCP_STATE_CLOSE_WAIT}""`, LOG_FETCH); reply.tcp.ack = true; - if(this.send_buffer.length || this.pending) { - this.state = TCP_STATE_CLOSE_WAIT; - this.delayed_send_fin = true; + this.state = TCP_STATE_CLOSE_WAIT; + // pass the passive close event up to the NetworkAdapter, this event is intended for the remote end + // TODO: method *NetworkAdapter.on_passive_close(), implementation: + // - WispNetworkAdapter: as below + // - FetchNetworkAdapter: not sure, maybe just an empty method? + if(this.net instanceof WispNetworkAdapter) { + this.net.send_wisp_frame({ + type: "CLOSE", + stream_id: this.stream_id, + reason: 0x02 // 0x02: Voluntary stream closure + }); + } + } + else if(this.state === TCP_STATE_FIN_WAIT_1) { + if(packet.tcp.ack) { + // dbg_log(`TCP[${this.tuple}]: received ACK+FIN in state "${this.state}"`, LOG_FETCH); + this.release(); } else { - dbg_log(`TCP[${this.tuple}]: sending FIN in state "${this.state}", next "${TCP_STATE_LAST_ACK}"`, LOG_FETCH); - this.state = TCP_STATE_LAST_ACK; - reply.tcp.fin = true; + // dbg_log(`TCP[${this.tuple}]: received ACK in state "${this.state}", next "${TCP_STATE_CLOSING}"`, LOG_FETCH); + this.state = TCP_STATE_CLOSING; } + reply.tcp.ack = true; } else if(this.state === TCP_STATE_FIN_WAIT_2) { + // dbg_log(`TCP[${this.tuple}]: received FIN in state "${this.state}"`, LOG_FETCH); this.release(); reply.tcp.ack = true; } else { - dbg_log(`TCP[${this.tuple}]: ERROR: received FIN in unexpected TCP state "${this.state}", resetting`, LOG_FETCH); + // dbg_log(`TCP[${this.tuple}]: ERROR: received FIN in unexpected TCP state "${this.state}", resetting`, LOG_FETCH); this.release(); reply.tcp.rst = true; } @@ -1206,19 +1201,38 @@ TCPConnection.prototype.process = function(packet) { * @param {Uint8Array} data */ TCPConnection.prototype.write = function(data) { - this.send_buffer.write(data); + if(!this.in_active_close) { + this.send_buffer.write(data); + } this.pump(); }; TCPConnection.prototype.close = function() { - if(this.state === TCP_STATE_ESTABLISHED && !this.delayed_send_fin) { + if(!this.in_active_close) { + this.in_active_close = true; + let next_state; + if(this.state === TCP_STATE_ESTABLISHED || this.state === TCP_STATE_SYN_RECEIVED) { + next_state = TCP_STATE_FIN_WAIT_1; + } + else if(this.state === TCP_STATE_CLOSE_WAIT) { + next_state = TCP_STATE_LAST_ACK; + } + else { + if(this.state !== TCP_STATE_SYN_SENT) { + dbg_log(`TCP[${this.tuple}]: active close in unexpected state "${this.state}"`, LOG_FETCH); + } + this.release(); + return; + } + if(this.send_buffer.length || this.pending) { - dbg_log(`TCP[${this.tuple}]: active close, delaying FIN in state "${this.state}"`, LOG_FETCH); + // dbg_log(`TCP[${this.tuple}]: active close, delaying FIN in state "${this.state}", delayed next "${next_state}"`, LOG_FETCH); this.delayed_send_fin = true; + this.delayed_state = next_state; } else { - dbg_log(`TCP[${this.tuple}]: active close, sending FIN in state "${this.state}", next "${TCP_STATE_FIN_WAIT_1}"`, LOG_FETCH); - this.state = TCP_STATE_FIN_WAIT_1; + // dbg_log(`TCP[${this.tuple}]: active close, sending FIN in state "${this.state}", next "${next_state}"`, LOG_FETCH); + this.state = next_state; const reply = this.ipv4_reply(); reply.tcp.fin = true; this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); @@ -1229,7 +1243,7 @@ TCPConnection.prototype.close = function() { TCPConnection.prototype.release = function() { if(this.net.tcp_conn[this.tuple]) { - dbg_log(`TCP[${this.tuple}]: connection closed in state "${this.state}"`, LOG_FETCH); + // dbg_log(`TCP[${this.tuple}]: connection closed in state "${this.state}"`, LOG_FETCH); this.state = TCP_STATE_CLOSED; delete this.net.tcp_conn[this.tuple]; } From a2d009b7f85b5947cea31db4457ec641b0008eea Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Sun, 17 Nov 2024 17:03:23 +0100 Subject: [PATCH 52/90] added now required call to close() once fetch() is complete --- src/browser/fetch_network.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/browser/fetch_network.js b/src/browser/fetch_network.js index 3813f3dd28..6427e1b4b1 100644 --- a/src/browser/fetch_network.js +++ b/src/browser/fetch_network.js @@ -126,6 +126,7 @@ async function on_data_http(data) this.write(new TextEncoder().encode(lines.join("\r\n"))); this.write(new Uint8Array(ab)); + this.close(); } } From b72c697abeee06d4053074316a83f3552d524598 Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Mon, 18 Nov 2024 20:51:38 +0100 Subject: [PATCH 53/90] added method TCPConnection.writev([ Uint8Array, ... ]) Optimizes writing multiple buffers to the TCPConnection at once. --- src/browser/fake_network.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/browser/fake_network.js b/src/browser/fake_network.js index 827a578aa2..14a3d50294 100644 --- a/src/browser/fake_network.js +++ b/src/browser/fake_network.js @@ -1207,6 +1207,18 @@ TCPConnection.prototype.write = function(data) { this.pump(); }; +/** + * @param {!Array} data_array + */ +TCPConnection.prototype.writev = function(data_array) { + if(!this.in_active_close) { + for(const data of data_array) { + this.send_buffer.write(data); + } + } + this.pump(); +}; + TCPConnection.prototype.close = function() { if(!this.in_active_close) { this.in_active_close = true; From 53c43f77228799785ebe2e25f019731af24d9a3a Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Mon, 18 Nov 2024 21:06:24 +0100 Subject: [PATCH 54/90] use ReadableStream in fetch() response body handling - replaced blocking call to fetch() in on_data_http() with async loop, response body chunks are now forwarded as soon as they arrive - commented out old code path in on_data_http() (for fallback) - commented out FetchNetworkAdapter.fetch() (for fallback), merged into on_data_http() - removed code to make type checking happy for data argument in on_data_http(), fixed JSC declaration instead --- src/browser/fetch_network.js | 53 ++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/src/browser/fetch_network.js b/src/browser/fetch_network.js index 6427e1b4b1..513ed4b7fd 100644 --- a/src/browser/fetch_network.js +++ b/src/browser/fetch_network.js @@ -55,11 +55,10 @@ FetchNetworkAdapter.prototype.on_tcp_connection = function(adapter, packet, tupl /** * @this {TCPConnection} - * @param {ArrayBuffer} data + * @param {!ArrayBuffer} data */ async function on_data_http(data) { - if(!data) return; // Make type checking happy. this.read = this.read || ""; this.read += new TextDecoder().decode(data); if(this.read && this.read.indexOf("\r\n\r\n") !== -1) { @@ -103,6 +102,7 @@ async function on_data_http(data) if(["put", "post"].indexOf(opts.method.toLowerCase()) !== -1) { opts.body = data; } +/* const [resp, ab] = await this.net.fetch(target.href, opts); const lines = [ `HTTP/1.1 ${resp.status} ${resp.statusText}`, @@ -127,9 +127,57 @@ async function on_data_http(data) this.write(new TextEncoder().encode(lines.join("\r\n"))); this.write(new Uint8Array(ab)); this.close(); +*/ + const fetch_url = this.cors_proxy ? this.cors_proxy + encodeURIComponent(target.href) : target.href; + const encoder = new TextEncoder(); + let response_started = false; + fetch(fetch_url, opts).then((resp) => { + const header_lines = [ + `HTTP/1.1 ${resp.status} ${resp.statusText}`, + `x-was-fetch-redirected: ${!!resp.redirected}`, + `x-fetch-resp-url: ${resp.url}`, + "Connection: closed" + ]; + for(const [key, value] of resp.headers.entries()) { + if(!["content-encoding", "connection", "content-length", "transfer-encoding"].includes(key.toLowerCase())) { + header_lines.push(`${key}: ${value}`); + } + } + this.write(encoder.encode(header_lines.join("\r\n") + "\r\n\r\n")); + response_started = true; + + const resp_reader = resp.body.getReader(); + const pump = ({ value, done }) => { + if(value) { + this.write(value); + } + if(done) { + this.close(); + } + else { + return resp_reader.read().then(pump); + } + }; + resp_reader.read().then(pump); + }) + .catch((e) => { + console.warn("Fetch Failed: " + fetch_url + "\n" + e); + if(!response_started) { + const body = encoder.encode(`Fetch ${fetch_url} failed:\n\n${e.stack || e.message}`); + const header_lines = [ + "HTTP/1.1 502 Fetch Error", + "Content-Type: text/plain", + `Content-Length: ${body.length}`, + "Connection: closed" + ]; + this.writev([encoder.encode(header_lines.join("\r\n") + "\r\n\r\n"), body]); + } + this.close(); + }); } } +/* FetchNetworkAdapter.prototype.fetch = async function(url, options) { if(this.cors_proxy) url = this.cors_proxy + encodeURIComponent(url); @@ -155,6 +203,7 @@ FetchNetworkAdapter.prototype.fetch = async function(url, options) ]; } }; +*/ FetchNetworkAdapter.prototype.parse_http_header = function(header) { From 8788bf972f013114ac53d51f63d259a519d189a8 Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Tue, 19 Nov 2024 19:46:10 +0100 Subject: [PATCH 55/90] removed/solved a few leftover TODOs --- src/browser/fake_network.js | 25 ++++++------------------- src/browser/wisp_network.js | 8 ++++++++ 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/browser/fake_network.js b/src/browser/fake_network.js index 14a3d50294..27d08889f5 100644 --- a/src/browser/fake_network.js +++ b/src/browser/fake_network.js @@ -22,9 +22,6 @@ const V86_ASCII = [118, 56, 54]; * https://en.wikipedia.org/wiki/File:Tcp_state_diagram_fixed_new.svg * * State TIME_WAIT is not needed, we can skip it and transition directly to CLOSED instead. - * - * TODO: - * - What about state LISTEN? Neither WISP nor FETCH seem to support it. */ const TCP_STATE_CLOSED = "closed"; const TCP_STATE_SYN_RECEIVED = "syn-received"; @@ -249,7 +246,7 @@ function handle_fake_tcp(packet, adapter) if(packet.tcp.syn) { if(adapter.tcp_conn[tuple]) { - dbg_log("SYN to already opened port", LOG_FETCH); // TODO: is LOG_FETCH a good choice for this module? + dbg_log("SYN to already opened port", LOG_FETCH); } if(adapter.on_tcp_connection(adapter, packet, tuple)) { return; @@ -548,10 +545,7 @@ function parse_ipv4(data, o) { }; // Ethernet minmum packet size. - /* TODO: What's the overall reasoning behind this check, and where does the 46 come from? We get plenty of IP packets of size 40, 42, etc. if(Math.max(len, 46) !== data.length) { - */ - if(Math.max(len, 40) !== data.length) { dbg_log(`ipv4 Length mismatch: ${len} != ${data.length}`, LOG_FETCH); } @@ -1118,7 +1112,7 @@ TCPConnection.prototype.process = function(packet) { return; } } - else if(n_ack < 0) { // TODO: any better way to handle this? could this just be a 32-bit sequence number overflow? + else if(n_ack < 0) { // TODO: could this just be a 32-bit sequence number overflow? dbg_log(`TCP[${this.tuple}]: ERROR: ack underflow (pkt=${packet.tcp.ackn} last=${this.last_received_ackn}), resetting`, LOG_FETCH); const reply = this.packet_reply(packet, {rst: true}); this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); @@ -1137,17 +1131,7 @@ TCPConnection.prototype.process = function(packet) { // dbg_log(`TCP[${this.tuple}]: received FIN in state "${this.state}, next "${TCP_STATE_CLOSE_WAIT}""`, LOG_FETCH); reply.tcp.ack = true; this.state = TCP_STATE_CLOSE_WAIT; - // pass the passive close event up to the NetworkAdapter, this event is intended for the remote end - // TODO: method *NetworkAdapter.on_passive_close(), implementation: - // - WispNetworkAdapter: as below - // - FetchNetworkAdapter: not sure, maybe just an empty method? - if(this.net instanceof WispNetworkAdapter) { - this.net.send_wisp_frame({ - type: "CLOSE", - stream_id: this.stream_id, - reason: 0x02 // 0x02: Voluntary stream closure - }); - } + this.on_passive_close(); } else if(this.state === TCP_STATE_FIN_WAIT_1) { if(packet.tcp.ack) { @@ -1253,6 +1237,9 @@ TCPConnection.prototype.close = function() { this.pump(); }; +TCPConnection.prototype.on_passive_close = function() { +} + TCPConnection.prototype.release = function() { if(this.net.tcp_conn[this.tuple]) { // dbg_log(`TCP[${this.tuple}]: connection closed in state "${this.state}"`, LOG_FETCH); diff --git a/src/browser/wisp_network.js b/src/browser/wisp_network.js index 5b62dc03f9..4ff6b81291 100644 --- a/src/browser/wisp_network.js +++ b/src/browser/wisp_network.js @@ -219,6 +219,14 @@ WispNetworkAdapter.prototype.send = function(data) } }; + tcp_conn.on_passive_close = () => { + this.send_wisp_frame({ + type: "CLOSE", + stream_id: tcp_conn.stream_id, + reason: 0x02 // 0x02: Voluntary stream closure + }); + }; + this.send_wisp_frame({ type: "CONNECT", stream_id: tcp_conn.stream_id, From 51ecbefd9c2b125cbda305f08cd5789b24d4850e Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Tue, 19 Nov 2024 19:52:05 +0100 Subject: [PATCH 56/90] fixed eslint errors --- src/browser/fake_network.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/fake_network.js b/src/browser/fake_network.js index 27d08889f5..05b48f5bd1 100644 --- a/src/browser/fake_network.js +++ b/src/browser/fake_network.js @@ -1238,7 +1238,7 @@ TCPConnection.prototype.close = function() { }; TCPConnection.prototype.on_passive_close = function() { -} +}; TCPConnection.prototype.release = function() { if(this.net.tcp_conn[this.tuple]) { From 0e98beb82e07fe013cbb2ceb6d8a4ec0fb5ecced Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Thu, 21 Nov 2024 10:40:29 +0100 Subject: [PATCH 57/90] reactivated FetchNetworkAdapter.fetch() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Even though this method is currently only used ín a test, keep this function for possible future use. --- src/browser/fetch_network.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/browser/fetch_network.js b/src/browser/fetch_network.js index 513ed4b7fd..50a2809eac 100644 --- a/src/browser/fetch_network.js +++ b/src/browser/fetch_network.js @@ -177,7 +177,6 @@ async function on_data_http(data) } } -/* FetchNetworkAdapter.prototype.fetch = async function(url, options) { if(this.cors_proxy) url = this.cors_proxy + encodeURIComponent(url); @@ -203,7 +202,6 @@ FetchNetworkAdapter.prototype.fetch = async function(url, options) ]; } }; -*/ FetchNetworkAdapter.prototype.parse_http_header = function(header) { From 5a3b77d07476f8a9066c26bc6cc6a4cf48191718 Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Thu, 21 Nov 2024 10:42:06 +0100 Subject: [PATCH 58/90] removed method TCPConnection.on_passive_close() Even though this method is conceptually correct, it is useless because neither WISP nor fetch() support closing only our half of the TCP connection. --- src/browser/fake_network.js | 5 +---- src/browser/wisp_network.js | 8 -------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/browser/fake_network.js b/src/browser/fake_network.js index 05b48f5bd1..ec375bcbe5 100644 --- a/src/browser/fake_network.js +++ b/src/browser/fake_network.js @@ -1131,7 +1131,7 @@ TCPConnection.prototype.process = function(packet) { // dbg_log(`TCP[${this.tuple}]: received FIN in state "${this.state}, next "${TCP_STATE_CLOSE_WAIT}""`, LOG_FETCH); reply.tcp.ack = true; this.state = TCP_STATE_CLOSE_WAIT; - this.on_passive_close(); + // NOTE that we should forward the CLOSE event from the guest here, but neither WISP nor fetch() support that } else if(this.state === TCP_STATE_FIN_WAIT_1) { if(packet.tcp.ack) { @@ -1237,9 +1237,6 @@ TCPConnection.prototype.close = function() { this.pump(); }; -TCPConnection.prototype.on_passive_close = function() { -}; - TCPConnection.prototype.release = function() { if(this.net.tcp_conn[this.tuple]) { // dbg_log(`TCP[${this.tuple}]: connection closed in state "${this.state}"`, LOG_FETCH); diff --git a/src/browser/wisp_network.js b/src/browser/wisp_network.js index 4ff6b81291..5b62dc03f9 100644 --- a/src/browser/wisp_network.js +++ b/src/browser/wisp_network.js @@ -219,14 +219,6 @@ WispNetworkAdapter.prototype.send = function(data) } }; - tcp_conn.on_passive_close = () => { - this.send_wisp_frame({ - type: "CLOSE", - stream_id: tcp_conn.stream_id, - reason: 0x02 // 0x02: Voluntary stream closure - }); - }; - this.send_wisp_frame({ type: "CONNECT", stream_id: tcp_conn.stream_id, From f820081a3746ced5b7ed76e3ede67dccf1b635cb Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Fri, 22 Nov 2024 17:35:50 +0100 Subject: [PATCH 59/90] added TCPConnection.on_shutdown() and TCPConnection.on_close() - both methods must be overloaded by the network adapter, just like TCPConnection.on_data() - on_shutdown() informs the network adapter that the client has closed its half of the connection (FIN) - on_close() informs the network adapter that the client has fully closed the connection (RST) - added call to on_shutdown() when we receive a TCP packet with active FIN flag from guest - added call to on_close() when we receive a TCP packet with active RST flag from guest - added calls to on_close() when we have to tear down the connection in fake_network.js - added implementation of on_shutdown() and on_close() in wisp_network.js The default implementation of these methods is to do nothing. These methods do not apply to fetch-based networking, fetch() only supports HTTP and it doesn't matter if the client closes its end of the connection after it has sent its HTTP request. Hence FetchNetworkAdapter does not override these methods. Note that WISP currently only supports close(), as a workaround shutdown() is implemented like close() which might be incorrect (missing support for half-closed TCP connections). --- src/browser/fake_network.js | 13 ++++++++++++- src/browser/wisp_network.js | 32 +++++++++++++++++++++++++------- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/browser/fake_network.js b/src/browser/fake_network.js index ec375bcbe5..5fee61d15c 100644 --- a/src/browser/fake_network.js +++ b/src/browser/fake_network.js @@ -1050,6 +1050,7 @@ TCPConnection.prototype.process = function(packet) { } else if(packet.tcp.rst) { // dbg_log(`TCP[${this.tuple}]: received RST in state "${this.state}"`, LOG_FETCH); + this.on_close(); this.release(); return; } @@ -1116,6 +1117,7 @@ TCPConnection.prototype.process = function(packet) { dbg_log(`TCP[${this.tuple}]: ERROR: ack underflow (pkt=${packet.tcp.ackn} last=${this.last_received_ackn}), resetting`, LOG_FETCH); const reply = this.packet_reply(packet, {rst: true}); this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); + this.on_close(); this.release(); return; } @@ -1131,7 +1133,7 @@ TCPConnection.prototype.process = function(packet) { // dbg_log(`TCP[${this.tuple}]: received FIN in state "${this.state}, next "${TCP_STATE_CLOSE_WAIT}""`, LOG_FETCH); reply.tcp.ack = true; this.state = TCP_STATE_CLOSE_WAIT; - // NOTE that we should forward the CLOSE event from the guest here, but neither WISP nor fetch() support that + this.on_shutdown(); } else if(this.state === TCP_STATE_FIN_WAIT_1) { if(packet.tcp.ack) { @@ -1152,6 +1154,7 @@ TCPConnection.prototype.process = function(packet) { else { // dbg_log(`TCP[${this.tuple}]: ERROR: received FIN in unexpected TCP state "${this.state}", resetting`, LOG_FETCH); this.release(); + this.on_close(); reply.tcp.rst = true; } this.net.receive(make_packet(this.net.eth_encoder_buf, reply)); @@ -1237,6 +1240,14 @@ TCPConnection.prototype.close = function() { this.pump(); }; +TCPConnection.prototype.on_shutdown = function() { + // forward FIN event from guest to network provider +}; + +TCPConnection.prototype.on_close = function() { + // forward RST event from guest to network provider +}; + TCPConnection.prototype.release = function() { if(this.net.tcp_conn[this.tuple]) { // dbg_log(`TCP[${this.tuple}]: connection closed in state "${this.state}"`, LOG_FETCH); diff --git a/src/browser/wisp_network.js b/src/browser/wisp_network.js index 5b62dc03f9..14657ef381 100644 --- a/src/browser/wisp_network.js +++ b/src/browser/wisp_network.js @@ -51,14 +51,16 @@ WispNetworkAdapter.prototype.register_ws = function(wisp_url) { }; WispNetworkAdapter.prototype.send_packet = function(data, type, stream_id) { - if(this.connections[stream_id].congestion > 0) { - if(type === "DATA") { - this.connections[stream_id].congestion--; + if(this.connections[stream_id]) { + if(this.connections[stream_id].congestion > 0) { + if(type === "DATA") { + this.connections[stream_id].congestion--; + } + this.wispws.send(data); + } else { + this.connections[stream_id].congested = true; + this.congested_buffer.push({data: data, type: type}); } - this.wispws.send(data); - } else { - this.connections[stream_id].congested = true; - this.congested_buffer.push({data: data, type: type}); } }; @@ -219,6 +221,22 @@ WispNetworkAdapter.prototype.send = function(data) } }; + tcp_conn.on_shutdown = () => { + this.send_wisp_frame({ + type: "CLOSE", + stream_id: tcp_conn.stream_id, + reason: 0x02 // 0x02: Voluntary stream closure + }); + }; + + tcp_conn.on_close = () => { + this.send_wisp_frame({ + type: "CLOSE", + stream_id: tcp_conn.stream_id, + reason: 0x02 // 0x02: Voluntary stream closure + }); + }; + this.send_wisp_frame({ type: "CONNECT", stream_id: tcp_conn.stream_id, From 31d46c0c122d7473199e4b6f8494cea80e8721a0 Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Sun, 24 Nov 2024 16:14:34 +0100 Subject: [PATCH 60/90] simplified control flow in handle_fake_networking() and WispNetworkAdapter.send() - in both functions, all the scattered "if" statements are mutually exclusive - changed control flow to mutual exclusive cases - removed boolean return values in handle_fake_networking(), they were never checked by the caller --- src/browser/fake_network.js | 50 ++++----- src/browser/wisp_network.js | 216 ++++++++++++++++++------------------ 2 files changed, 127 insertions(+), 139 deletions(-) diff --git a/src/browser/fake_network.js b/src/browser/fake_network.js index 5fee61d15c..806cd0c79c 100644 --- a/src/browser/fake_network.js +++ b/src/browser/fake_network.js @@ -404,41 +404,31 @@ function handle_fake_dhcp(packet, adapter) { function handle_fake_networking(data, adapter) { let packet = {}; parse_eth(data, packet); - if(packet.tcp) { - if(handle_fake_tcp(packet, adapter)) { - return true; - } - } - - if(packet.arp && packet.arp.oper === 1 && packet.arp.ptype === ETHERTYPE_IPV4) { - arp_whohas(packet, adapter); - } - if(packet.dns) { - if(handle_fake_dns(packet, adapter)) { - return; + if(packet.ipv4) { + if(packet.tcp) { + handle_fake_tcp(packet, adapter); } - } - - if(packet.ntp) { - if(handle_fake_ntp(packet, adapter)) { - return; + else if(packet.udp) { + if(packet.dns) { + handle_fake_dns(packet, adapter); + } + else if(packet.dhcp) { + handle_fake_dhcp(packet, adapter); + } + else if(packet.ntp) { + handle_fake_ntp(packet, adapter); + } + else if(packet.udp.dport === 8) { + handle_udp_echo(packet, adapter); + } } - } - - // ICMP Ping - if(packet.icmp && packet.icmp.type === 8) { - handle_fake_ping(packet, adapter); - } - - if(packet.dhcp) { - if(handle_fake_dhcp(packet, adapter)) { - return; + else if(packet.icmp && packet.icmp.type === 8) { + handle_fake_ping(packet, adapter); } } - - if(packet.udp && packet.udp.dport === 8) { - handle_udp_echo(packet, adapter); + else if(packet.arp && packet.arp.oper === 1 && packet.arp.ptype === ETHERTYPE_IPV4) { + arp_whohas(packet, adapter); } } diff --git a/src/browser/wisp_network.js b/src/browser/wisp_network.js index 14657ef381..ab53b7d3ee 100644 --- a/src/browser/wisp_network.js +++ b/src/browser/wisp_network.js @@ -183,133 +183,131 @@ WispNetworkAdapter.prototype.send = function(data) let packet = {}; parse_eth(data, packet); - if(packet.tcp) { - let reply = {}; - reply.eth = { ethertype: ETHERTYPE_IPV4, src: this.router_mac, dest: packet.eth.src }; - reply.ipv4 = { - proto: IPV4_PROTO_TCP, - src: packet.ipv4.dest, - dest: packet.ipv4.src - }; - - let tuple = [ - packet.ipv4.src.join("."), - packet.tcp.sport, - packet.ipv4.dest.join("."), - packet.tcp.dport - ].join(":"); + if(packet.ipv4) { + if(packet.tcp) { + let reply = {}; + reply.eth = { ethertype: ETHERTYPE_IPV4, src: this.router_mac, dest: packet.eth.src }; + reply.ipv4 = { + proto: IPV4_PROTO_TCP, + src: packet.ipv4.dest, + dest: packet.ipv4.src + }; - if(packet.tcp.syn) { - if(this.tcp_conn[tuple]) { - dbg_log("SYN to already opened port", LOG_FETCH); - } - const tcp_conn = new TCPConnection(); + let tuple = [ + packet.ipv4.src.join("."), + packet.tcp.sport, + packet.ipv4.dest.join("."), + packet.tcp.dport + ].join(":"); - tcp_conn.state = TCP_STATE_SYN_RECEIVED; - tcp_conn.net = this; - tcp_conn.tuple = tuple; - tcp_conn.stream_id = this.last_stream++; - this.tcp_conn[tuple] = tcp_conn; + if(packet.tcp.syn) { + if(this.tcp_conn[tuple]) { + dbg_log("SYN to already opened port", LOG_FETCH); + } + const tcp_conn = new TCPConnection(); + + tcp_conn.state = TCP_STATE_SYN_RECEIVED; + tcp_conn.net = this; + tcp_conn.tuple = tuple; + tcp_conn.stream_id = this.last_stream++; + this.tcp_conn[tuple] = tcp_conn; + + tcp_conn.on_data = (data) => { + if(data.length !== 0) { + this.send_wisp_frame({ + type: "DATA", + stream_id: tcp_conn.stream_id, + data: data + }); + } + }; + + tcp_conn.on_shutdown = () => { + this.send_wisp_frame({ + type: "CLOSE", + stream_id: tcp_conn.stream_id, + reason: 0x02 // 0x02: Voluntary stream closure + }); + }; - tcp_conn.on_data = (data) => { - if(data.length !== 0) { + tcp_conn.on_close = () => { this.send_wisp_frame({ - type: "DATA", + type: "CLOSE", stream_id: tcp_conn.stream_id, - data: data + reason: 0x02 // 0x02: Voluntary stream closure }); - } - }; + }; - tcp_conn.on_shutdown = () => { this.send_wisp_frame({ - type: "CLOSE", + type: "CONNECT", stream_id: tcp_conn.stream_id, - reason: 0x02 // 0x02: Voluntary stream closure + hostname: packet.ipv4.dest.join("."), + port: packet.tcp.dport, + data_callback: (data) => { + tcp_conn.write(data); + }, + close_callback: (data) => { + tcp_conn.close(); + } }); - }; - tcp_conn.on_close = () => { - this.send_wisp_frame({ - type: "CLOSE", - stream_id: tcp_conn.stream_id, - reason: 0x02 // 0x02: Voluntary stream closure - }); - }; + tcp_conn.accept(packet); + return; + } - this.send_wisp_frame({ - type: "CONNECT", - stream_id: tcp_conn.stream_id, - hostname: packet.ipv4.dest.join("."), - port: packet.tcp.dport, - data_callback: (data) => { - tcp_conn.write(data); - }, - close_callback: (data) => { - tcp_conn.close(); - } - }); + if(!this.tcp_conn[tuple]) { + dbg_log(`I dont know about ${tuple}, so restting`, LOG_FETCH); + let bop = packet.tcp.ackn; + if(packet.tcp.fin || packet.tcp.syn) bop += 1; + reply.tcp = { + sport: packet.tcp.dport, + dport: packet.tcp.sport, + seq: bop, + ackn: packet.tcp.seq + (packet.tcp.syn ? 1: 0), + winsize: packet.tcp.winsize, + rst: true, + ack: packet.tcp.syn + }; + this.receive(make_packet(this.eth_encoder_buf, reply)); + return; + } - tcp_conn.accept(packet); - return; + this.tcp_conn[tuple].process(packet); } - - if(!this.tcp_conn[tuple]) { - dbg_log(`I dont know about ${tuple}, so restting`, LOG_FETCH); - let bop = packet.tcp.ackn; - if(packet.tcp.fin || packet.tcp.syn) bop += 1; - reply.tcp = { - sport: packet.tcp.dport, - dport: packet.tcp.sport, - seq: bop, - ackn: packet.tcp.seq + (packet.tcp.syn ? 1: 0), - winsize: packet.tcp.winsize, - rst: true, - ack: packet.tcp.syn - }; - this.receive(make_packet(this.eth_encoder_buf, reply)); - return; + else if(packet.udp) { + // TODO: remove when this wisp client supports udp + if(packet.dns) { + (async () => { + let reply = {}; + reply.eth = { ethertype: ETHERTYPE_IPV4, src: this.router_mac, dest: packet.eth.src }; + reply.ipv4 = { + proto: IPV4_PROTO_UDP, + src: this.router_ip, + dest: packet.ipv4.src, + }; + reply.udp = { sport: 53, dport: packet.udp.sport }; + const result = await ((await fetch(`https://${this.doh_server}/dns-query`, {method: "POST", headers: [["content-type", "application/dns-message"]], body: packet.udp.data})).arrayBuffer()); + reply.udp.data = new Uint8Array(result); + this.receive(make_packet(this.eth_encoder_buf, reply)); + })(); + } + else if(packet.dhcp) { + handle_fake_dhcp(packet, this); + } + else if(packet.ntp) { + handle_fake_ntp(packet, this); + } + else if(packet.udp.dport === 8) { + handle_udp_echo(packet, this); + } + } + else if(packet.icmp && packet.icmp.type === 8) { + handle_fake_ping(packet, this); } - - this.tcp_conn[tuple].process(packet); } - - if(packet.arp && packet.arp.oper === 1 && packet.arp.ptype === ETHERTYPE_IPV4) { + else if(packet.arp && packet.arp.oper === 1 && packet.arp.ptype === ETHERTYPE_IPV4) { arp_whohas(packet, this); } - - if(packet.dns) { - // TODO: remove when this wisp client supports udp - (async () => { - let reply = {}; - reply.eth = { ethertype: ETHERTYPE_IPV4, src: this.router_mac, dest: packet.eth.src }; - reply.ipv4 = { - proto: IPV4_PROTO_UDP, - src: this.router_ip, - dest: packet.ipv4.src, - }; - reply.udp = { sport: 53, dport: packet.udp.sport }; - const result = await ((await fetch(`https://${this.doh_server}/dns-query`, {method: "POST", headers: [["content-type", "application/dns-message"]], body: packet.udp.data})).arrayBuffer()); - reply.udp.data = new Uint8Array(result); - this.receive(make_packet(this.eth_encoder_buf, reply)); - })(); - } - - if(packet.ntp) { - // TODO: remove when this wisp client supports udp - handle_fake_ntp(packet, this); - return; - } - - if(packet.dhcp) { - handle_fake_dhcp(packet, this); - return; - } - - if(packet.udp && packet.udp.dport === 8) { - // TODO: remove when this wisp client supports udp - handle_udp_echo(packet, this); - } }; /** From bc39f6c8349f765d442dc5b6a33a1b70de6dba00 Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Sun, 24 Nov 2024 17:56:23 +0100 Subject: [PATCH 61/90] reduced code duplication in WispNetworkAdapter - added missing callback WispNetworkAdapter.on_tcp_connection(packet, tuple), moved all relevant code from WispNetworkAdapter.send(data) to new method, replaced with call to handle_fake_tcp() (now identical to FetchNetworkAdapter) - removed redundant first argument of adapter.on_tcp_connection(adapter, ...) - added TCP_STATE_SYN_RECEIVED -> TCP_STATE_ESTABLISHED state transition back in (just to be sure) The larger goal is to replace all code in WispNetworkAdapter.send(data) with a call to handle_fake_networking(), just like in FetchNetworkAdapter. The only difference left is two alternate fake DNS implementations. --- src/browser/fake_network.js | 22 +++-- src/browser/fetch_network.js | 4 +- src/browser/wisp_network.js | 168 ++++++++++++++--------------------- 3 files changed, 81 insertions(+), 113 deletions(-) diff --git a/src/browser/fake_network.js b/src/browser/fake_network.js index 806cd0c79c..35a2613866 100644 --- a/src/browser/fake_network.js +++ b/src/browser/fake_network.js @@ -235,20 +235,13 @@ function make_packet(out, spec) function handle_fake_tcp(packet, adapter) { - let reply = {}; - reply.eth = { ethertype: ETHERTYPE_IPV4, src: adapter.router_mac, dest: packet.eth.src }; - reply.ipv4 = { - proto: IPV4_PROTO_TCP, - src: packet.ipv4.dest, - dest: packet.ipv4.src - }; const tuple = `${packet.ipv4.src.join(".")}:${packet.tcp.sport}:${packet.ipv4.dest.join(".")}:${packet.tcp.dport}`; if(packet.tcp.syn) { if(adapter.tcp_conn[tuple]) { dbg_log("SYN to already opened port", LOG_FETCH); } - if(adapter.on_tcp_connection(adapter, packet, tuple)) { + if(adapter.on_tcp_connection(packet, tuple)) { return; } } @@ -257,6 +250,13 @@ function handle_fake_tcp(packet, adapter) dbg_log(`I dont know about ${tuple}, so resetting`, LOG_FETCH); let bop = packet.tcp.ackn; if(packet.tcp.fin || packet.tcp.syn) bop += 1; + let reply = {}; + reply.eth = { ethertype: ETHERTYPE_IPV4, src: adapter.router_mac, dest: packet.eth.src }; + reply.ipv4 = { + proto: IPV4_PROTO_TCP, + src: packet.ipv4.dest, + dest: packet.ipv4.src + }; reply.tcp = { sport: packet.tcp.dport, dport: packet.tcp.sport, @@ -1068,7 +1068,11 @@ TCPConnection.prototype.process = function(packet) { } if(packet.tcp.ack) { - if(this.state === TCP_STATE_FIN_WAIT_1) { + if(this.state === TCP_STATE_SYN_RECEIVED) { + // dbg_log(`TCP[${this.tuple}]: received ACK in state "${this.state}", next "${TCP_STATE_ESTABLISHED}"`, LOG_FETCH); + this.state = TCP_STATE_ESTABLISHED; + } + else if(this.state === TCP_STATE_FIN_WAIT_1) { if(!packet.tcp.fin) { // handle FIN+ACK in FIN_WAIT_1 separately further down below // dbg_log(`TCP[${this.tuple}]: received ACK in state "${this.state}", next "${TCP_STATE_FIN_WAIT_2}"`, LOG_FETCH); this.state = TCP_STATE_FIN_WAIT_2; diff --git a/src/browser/fetch_network.js b/src/browser/fetch_network.js index 50a2809eac..f5e5a0227b 100644 --- a/src/browser/fetch_network.js +++ b/src/browser/fetch_network.js @@ -38,7 +38,7 @@ FetchNetworkAdapter.prototype.destroy = function() { }; -FetchNetworkAdapter.prototype.on_tcp_connection = function(adapter, packet, tuple) +FetchNetworkAdapter.prototype.on_tcp_connection = function(packet, tuple) { if(packet.tcp.dport === 80) { let conn = new TCPConnection(); @@ -47,7 +47,7 @@ FetchNetworkAdapter.prototype.on_tcp_connection = function(adapter, packet, tupl conn.on_data = on_data_http; conn.tuple = tuple; conn.accept(packet); - adapter.tcp_conn[tuple] = conn; + this.tcp_conn[tuple] = conn; return true; } return false; diff --git a/src/browser/wisp_network.js b/src/browser/wisp_network.js index ab53b7d3ee..e850ee5abd 100644 --- a/src/browser/wisp_network.js +++ b/src/browser/wisp_network.js @@ -131,10 +131,10 @@ WispNetworkAdapter.prototype.send_wisp_frame = function(frame_obj) { const hostname_buffer = new TextEncoder().encode(frame_obj.hostname); full_packet = new Uint8Array(5 + 1 + 2 + hostname_buffer.length); view = new DataView(full_packet.buffer); - view.setUint8(0, 0x01); // TYPE + view.setUint8(0, 0x01); // TYPE view.setUint32(1, frame_obj.stream_id, true); // Stream ID - view.setUint8(5, 0x01); // TCP - view.setUint16(6, frame_obj.port, true); // PORT + view.setUint8(5, 0x01); // TCP + view.setUint16(6, frame_obj.port, true); // PORT full_packet.set(hostname_buffer, 8); // hostname // Setting callbacks @@ -147,16 +147,16 @@ WispNetworkAdapter.prototype.send_wisp_frame = function(frame_obj) { case "DATA": full_packet = new Uint8Array(5 + frame_obj.data.length); view = new DataView(full_packet.buffer); - view.setUint8(0, 0x02); // TYPE + view.setUint8(0, 0x02); // TYPE view.setUint32(1, frame_obj.stream_id, true); // Stream ID full_packet.set(frame_obj.data, 5); // Actual data break; case "CLOSE": full_packet = new Uint8Array(5 + 1); view = new DataView(full_packet.buffer); - view.setUint8(0, 0x04); // TYPE + view.setUint8(0, 0x04); // TYPE view.setUint32(1, frame_obj.stream_id, true); // Stream ID - view.setUint8(5, frame_obj.reason); // Packet size + view.setUint8(5, frame_obj.reason); // Packet size break; default: dbg_log("Client tried to send unknown packet: " + frame_obj.type, LOG_NET); @@ -176,117 +176,81 @@ WispNetworkAdapter.prototype.destroy = function() }; /** - * @param {Uint8Array} data + * @param {Uint8Array} packet + * @param {String} tuple */ -WispNetworkAdapter.prototype.send = function(data) +WispNetworkAdapter.prototype.on_tcp_connection = function(packet, tuple) { - let packet = {}; - parse_eth(data, packet); - - if(packet.ipv4) { - if(packet.tcp) { - let reply = {}; - reply.eth = { ethertype: ETHERTYPE_IPV4, src: this.router_mac, dest: packet.eth.src }; - reply.ipv4 = { - proto: IPV4_PROTO_TCP, - src: packet.ipv4.dest, - dest: packet.ipv4.src - }; - - let tuple = [ - packet.ipv4.src.join("."), - packet.tcp.sport, - packet.ipv4.dest.join("."), - packet.tcp.dport - ].join(":"); - - if(packet.tcp.syn) { - if(this.tcp_conn[tuple]) { - dbg_log("SYN to already opened port", LOG_FETCH); - } - const tcp_conn = new TCPConnection(); + let conn = new TCPConnection(); + conn.state = TCP_STATE_SYN_RECEIVED; + conn.net = this; + conn.tuple = tuple; + conn.stream_id = this.last_stream++; + this.tcp_conn[tuple] = conn; - tcp_conn.state = TCP_STATE_SYN_RECEIVED; - tcp_conn.net = this; - tcp_conn.tuple = tuple; - tcp_conn.stream_id = this.last_stream++; - this.tcp_conn[tuple] = tcp_conn; - - tcp_conn.on_data = (data) => { - if(data.length !== 0) { - this.send_wisp_frame({ - type: "DATA", - stream_id: tcp_conn.stream_id, - data: data - }); - } - }; + conn.on_data = (data) => { + if(data.length !== 0) { + this.send_wisp_frame({ + type: "DATA", + stream_id: conn.stream_id, + data: data + }); + } + }; - tcp_conn.on_shutdown = () => { - this.send_wisp_frame({ - type: "CLOSE", - stream_id: tcp_conn.stream_id, - reason: 0x02 // 0x02: Voluntary stream closure - }); - }; + conn.on_close = () => { + this.send_wisp_frame({ + type: "CLOSE", + stream_id: conn.stream_id, + reason: 0x02 // 0x02: Voluntary stream closure + }); + }; - tcp_conn.on_close = () => { - this.send_wisp_frame({ - type: "CLOSE", - stream_id: tcp_conn.stream_id, - reason: 0x02 // 0x02: Voluntary stream closure - }); - }; + // WISP doesn't implement shutdown, use close as workaround + conn.on_shutdown = conn.on_close; - this.send_wisp_frame({ - type: "CONNECT", - stream_id: tcp_conn.stream_id, - hostname: packet.ipv4.dest.join("."), - port: packet.tcp.dport, - data_callback: (data) => { - tcp_conn.write(data); - }, - close_callback: (data) => { - tcp_conn.close(); - } - }); + this.send_wisp_frame({ + type: "CONNECT", + stream_id: conn.stream_id, + hostname: packet.ipv4.dest.join("."), + port: packet.tcp.dport, + data_callback: (data) => { + conn.write(data); + }, + close_callback: (data) => { + conn.close(); + } + }); - tcp_conn.accept(packet); - return; - } + conn.accept(packet); + return true; +} - if(!this.tcp_conn[tuple]) { - dbg_log(`I dont know about ${tuple}, so restting`, LOG_FETCH); - let bop = packet.tcp.ackn; - if(packet.tcp.fin || packet.tcp.syn) bop += 1; - reply.tcp = { - sport: packet.tcp.dport, - dport: packet.tcp.sport, - seq: bop, - ackn: packet.tcp.seq + (packet.tcp.syn ? 1: 0), - winsize: packet.tcp.winsize, - rst: true, - ack: packet.tcp.syn - }; - this.receive(make_packet(this.eth_encoder_buf, reply)); - return; - } +/** + * @param {Uint8Array} data + */ +WispNetworkAdapter.prototype.send = function(data) +{ + let packet = {}; + parse_eth(data, packet); - this.tcp_conn[tuple].process(packet); + if(packet.ipv4) { + if(packet.tcp) { + handle_fake_tcp(packet, this); } else if(packet.udp) { // TODO: remove when this wisp client supports udp if(packet.dns) { (async () => { - let reply = {}; - reply.eth = { ethertype: ETHERTYPE_IPV4, src: this.router_mac, dest: packet.eth.src }; - reply.ipv4 = { - proto: IPV4_PROTO_UDP, - src: this.router_ip, - dest: packet.ipv4.src, + const reply = { + eth: { ethertype: ETHERTYPE_IPV4, src: this.router_mac, dest: packet.eth.src }, + ipv4: { proto: IPV4_PROTO_UDP, src: this.router_ip, dest: packet.ipv4.src }, + udp: { sport: 53, dport: packet.udp.sport } }; - reply.udp = { sport: 53, dport: packet.udp.sport }; - const result = await ((await fetch(`https://${this.doh_server}/dns-query`, {method: "POST", headers: [["content-type", "application/dns-message"]], body: packet.udp.data})).arrayBuffer()); + const result = await (await fetch(`https://${this.doh_server}/dns-query`, { + method: "POST", + headers: [["content-type", "application/dns-message"]], + body: packet.udp.data})).arrayBuffer(); reply.udp.data = new Uint8Array(result); this.receive(make_packet(this.eth_encoder_buf, reply)); })(); From 31b5f662508df5d87c8a406559c6d18aa308e154 Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Sun, 24 Nov 2024 18:09:57 +0100 Subject: [PATCH 62/90] fixed eslint error --- src/browser/wisp_network.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/wisp_network.js b/src/browser/wisp_network.js index e850ee5abd..b752812dd0 100644 --- a/src/browser/wisp_network.js +++ b/src/browser/wisp_network.js @@ -224,7 +224,7 @@ WispNetworkAdapter.prototype.on_tcp_connection = function(packet, tuple) conn.accept(packet); return true; -} +}; /** * @param {Uint8Array} data From 7138331b8c60ee2b1cf7fd527065520e634812d1 Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Mon, 25 Nov 2024 18:05:45 +0100 Subject: [PATCH 63/90] moved DoH-code to fake_network.js - added member "dns_method" to both FetchNetworkAdapter (value "static") and WispNetworkAdapter (value "doh") - moved the DoH-code from WispNetworkAdapter.send() into new function handle_fake_dns_doh() in fake_network.js - renamed function handle_fake_dns() into handle_fake_dns_static() - recreated function handle_fake_dns() that now calls either of the two depending on the value of adapter.dns_method --- src/browser/fake_network.js | 49 ++++++++++++++++++++++++++++++--- src/browser/fetch_network.js | 2 +- src/browser/wisp_network.js | 52 ++++-------------------------------- 3 files changed, 52 insertions(+), 51 deletions(-) diff --git a/src/browser/fake_network.js b/src/browser/fake_network.js index 35a2613866..1b2c8a2b37 100644 --- a/src/browser/fake_network.js +++ b/src/browser/fake_network.js @@ -56,6 +56,8 @@ const TCP_PAYLOAD_OFFSET = IPV4_PAYLOAD_OFFSET + TCP_HEADER_SIZE; const TCP_PAYLOAD_SIZE = IPV4_PAYLOAD_SIZE - TCP_HEADER_SIZE; const ICMP_HEADER_SIZE = 4; +const DEFAULT_DOH_SERVER = "cloudflare-dns.com"; + function a2ethaddr(bytes) { return [0,1,2,3,4,5].map((i) => bytes[i].toString(16)).map(x => x.length === 1 ? "0" + x : x).join(":"); } @@ -273,7 +275,7 @@ function handle_fake_tcp(packet, adapter) adapter.tcp_conn[tuple].process(packet); } -function handle_fake_dns(packet, adapter) +function handle_fake_dns_static(packet, adapter) { let reply = {}; reply.eth = { ethertype: ETHERTYPE_IPV4, src: adapter.router_mac, dest: packet.eth.src }; @@ -316,6 +318,47 @@ function handle_fake_dns(packet, adapter) return true; } +function handle_fake_dns_doh(packet, adapter) +{ + const fetch_url = `https://${adapter.doh_server || DEFAULT_DOH_SERVER}/dns-query`; + const fetch_opts = { + method: "POST", + headers: [["content-type", "application/dns-message"]], + body: packet.udp.data + }; + fetch(fetch_url, fetch_opts).then(async (resp) => { + const reply = { + eth: { + ethertype: ETHERTYPE_IPV4, + src: adapter.router_mac, + dest: packet.eth.src + }, + ipv4: { + proto: IPV4_PROTO_UDP, + src: adapter.router_ip, + dest: packet.ipv4.src + }, + udp: { + sport: 53, + dport: packet.udp.sport, + data: new Uint8Array(await resp.arrayBuffer()) + } + }; + adapter.receive(make_packet(adapter.eth_encoder_buf, reply)); + }); + return true; +} + +function handle_fake_dns(packet, adapter) +{ + if(adapter.dns_method === 'static') { + return handle_fake_dns_static(packet, adapter); + } + else { + return handle_fake_dns_doh(packet, adapter); + } +} + function handle_fake_ntp(packet, adapter) { let now = Date.now(); // - 1000 * 60 * 60 * 24 * 7; let now_n = now + NTP_EPOC_DIFF; @@ -1235,11 +1278,11 @@ TCPConnection.prototype.close = function() { }; TCPConnection.prototype.on_shutdown = function() { - // forward FIN event from guest to network provider + // forward FIN event from guest device to network adapter }; TCPConnection.prototype.on_close = function() { - // forward RST event from guest to network provider + // forward RST event from guest device to network adapter }; TCPConnection.prototype.release = function() { diff --git a/src/browser/fetch_network.js b/src/browser/fetch_network.js index f5e5a0227b..6c2cbfde28 100644 --- a/src/browser/fetch_network.js +++ b/src/browser/fetch_network.js @@ -16,9 +16,9 @@ function FetchNetworkAdapter(bus, config) this.vm_ip = new Uint8Array((config.vm_ip || "192.168.86.100").split(".").map(function(x) { return parseInt(x, 10); })); this.masquerade = config.masquerade === undefined || !!config.masquerade; this.vm_mac = new Uint8Array(6); - this.tcp_conn = {}; this.eth_encoder_buf = create_eth_encoder_buf(); + this.dns_method = "static"; // Ex: 'https://corsproxy.io/?' this.cors_proxy = config.cors_proxy; diff --git a/src/browser/wisp_network.js b/src/browser/wisp_network.js index b752812dd0..1999e0cdd5 100644 --- a/src/browser/wisp_network.js +++ b/src/browser/wisp_network.js @@ -1,7 +1,5 @@ "use strict"; -const DEFAULT_DOH_SERVER = "cloudflare-dns.com"; - /** * @constructor * @@ -10,7 +8,6 @@ const DEFAULT_DOH_SERVER = "cloudflare-dns.com"; */ function WispNetworkAdapter(wisp_url, bus, config) { - this.register_ws(wisp_url); this.last_stream = 1; this.connections = {0: {congestion: 0}}; @@ -24,15 +21,15 @@ function WispNetworkAdapter(wisp_url, bus, config) this.vm_ip = new Uint8Array((config.vm_ip || "192.168.86.100").split(".").map(function(x) { return parseInt(x, 10); })); this.masquerade = config.masquerade === undefined || !!config.masquerade; this.vm_mac = new Uint8Array(6); - this.doh_server = config.doh_server || DEFAULT_DOH_SERVER; this.tcp_conn = {}; this.eth_encoder_buf = create_eth_encoder_buf(); + this.dns_method = "doh"; + this.doh_server = config.doh_server; this.bus.register("net" + this.id + "-mac", function(mac) { this.vm_mac = new Uint8Array(mac.split(":").map(function(x) { return parseInt(x, 16); })); }, this); - this.bus.register("net" + this.id + "-send", function(data) - { + this.bus.register("net" + this.id + "-send", function(data) { this.send(data); }, this); } @@ -231,47 +228,8 @@ WispNetworkAdapter.prototype.on_tcp_connection = function(packet, tuple) */ WispNetworkAdapter.prototype.send = function(data) { - let packet = {}; - parse_eth(data, packet); - - if(packet.ipv4) { - if(packet.tcp) { - handle_fake_tcp(packet, this); - } - else if(packet.udp) { - // TODO: remove when this wisp client supports udp - if(packet.dns) { - (async () => { - const reply = { - eth: { ethertype: ETHERTYPE_IPV4, src: this.router_mac, dest: packet.eth.src }, - ipv4: { proto: IPV4_PROTO_UDP, src: this.router_ip, dest: packet.ipv4.src }, - udp: { sport: 53, dport: packet.udp.sport } - }; - const result = await (await fetch(`https://${this.doh_server}/dns-query`, { - method: "POST", - headers: [["content-type", "application/dns-message"]], - body: packet.udp.data})).arrayBuffer(); - reply.udp.data = new Uint8Array(result); - this.receive(make_packet(this.eth_encoder_buf, reply)); - })(); - } - else if(packet.dhcp) { - handle_fake_dhcp(packet, this); - } - else if(packet.ntp) { - handle_fake_ntp(packet, this); - } - else if(packet.udp.dport === 8) { - handle_udp_echo(packet, this); - } - } - else if(packet.icmp && packet.icmp.type === 8) { - handle_fake_ping(packet, this); - } - } - else if(packet.arp && packet.arp.oper === 1 && packet.arp.ptype === ETHERTYPE_IPV4) { - arp_whohas(packet, this); - } + // TODO: forward UDP traffic to WISP server once this WISP client supports UDP + handle_fake_networking(data, this); }; /** From 26941f9cf2c6cc70e50472fb943ca0802ac5cd37 Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Mon, 25 Nov 2024 18:09:37 +0100 Subject: [PATCH 64/90] fixed eslint error --- src/browser/fake_network.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/fake_network.js b/src/browser/fake_network.js index 1b2c8a2b37..2947a78e1b 100644 --- a/src/browser/fake_network.js +++ b/src/browser/fake_network.js @@ -351,7 +351,7 @@ function handle_fake_dns_doh(packet, adapter) function handle_fake_dns(packet, adapter) { - if(adapter.dns_method === 'static') { + if(adapter.dns_method === "static") { return handle_fake_dns_static(packet, adapter); } else { From 298da237c2e8ae3b605ab60a5f873884a96ed054 Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Tue, 26 Nov 2024 22:28:54 +0100 Subject: [PATCH 65/90] fixed broken FetchNetworkAdapter.cors_prox attribute (#1187) --- src/browser/fetch_network.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/fetch_network.js b/src/browser/fetch_network.js index 6c2cbfde28..9af451bf5f 100644 --- a/src/browser/fetch_network.js +++ b/src/browser/fetch_network.js @@ -128,7 +128,7 @@ async function on_data_http(data) this.write(new Uint8Array(ab)); this.close(); */ - const fetch_url = this.cors_proxy ? this.cors_proxy + encodeURIComponent(target.href) : target.href; + const fetch_url = this.net.cors_proxy ? this.net.cors_proxy + encodeURIComponent(target.href) : target.href; const encoder = new TextEncoder(); let response_started = false; fetch(fetch_url, opts).then((resp) => { From e0e80779a8300523036533ea979a296058db4716 Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Wed, 11 Dec 2024 07:04:36 +0100 Subject: [PATCH 66/90] Allow embedding of CORS proxy in fetch relay_url (#1189) Extends the syntax of relay_url for fetch to: fetch[:CORS-PROXY-URL] where CORS-PROXY-URL is a full URL pointing to the CORS proxy server, for example: fetch:https://example.com/?url= * pass options.net_device to NetworkAdapter constructors - aligned construction of WispNetworkAdapter and FetchNetworkAdapter classes - supported members in object options.net_device (all optional): id, router_mac, router_ip, vm_ip, masquerade, dns_method, doh_server, cors_proxy - completed support for free choice between fake network DNS methods "static" and "doh" --- src/browser/fetch_network.js | 3 ++- src/browser/main.js | 6 ++++++ src/browser/starter.js | 4 ++-- src/browser/wisp_network.js | 5 +++-- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/browser/fetch_network.js b/src/browser/fetch_network.js index 9af451bf5f..8d0b980aba 100644 --- a/src/browser/fetch_network.js +++ b/src/browser/fetch_network.js @@ -16,9 +16,10 @@ function FetchNetworkAdapter(bus, config) this.vm_ip = new Uint8Array((config.vm_ip || "192.168.86.100").split(".").map(function(x) { return parseInt(x, 10); })); this.masquerade = config.masquerade === undefined || !!config.masquerade; this.vm_mac = new Uint8Array(6); + this.dns_method = config.dns_method || "static"; + this.doh_server = config.doh_server; this.tcp_conn = {}; this.eth_encoder_buf = create_eth_encoder_buf(); - this.dns_method = "static"; // Ex: 'https://corsproxy.io/?' this.cors_proxy = config.cors_proxy; diff --git a/src/browser/main.js b/src/browser/main.js index e9f469e103..15a88736ec 100644 --- a/src/browser/main.js +++ b/src/browser/main.js @@ -1737,6 +1737,11 @@ settings.relay_url = $("relay_url").value; if(!DEFAULT_NETWORKING_PROXIES.includes(settings.relay_url)) new_query_args.set("relay_url", settings.relay_url); } + if(settings.relay_url.startsWith("fetch:")) + { + settings.cors_proxy = settings.relay_url.slice(6); + settings.relay_url = "fetch"; + } settings.disable_audio = $("disable_audio").checked || settings.disable_audio; if(settings.disable_audio) new_query_args.set("mute", "1"); @@ -1852,6 +1857,7 @@ net_device: { type: settings.net_device_type || "ne2k", relay_url: settings.relay_url, + cors_proxy: settings.cors_proxy }, autostart: true, diff --git a/src/browser/starter.js b/src/browser/starter.js index 528a81bb63..47b9d9a61b 100644 --- a/src/browser/starter.js +++ b/src/browser/starter.js @@ -313,11 +313,11 @@ V86.prototype.continue_init = async function(emulator, options) // TODO: remove bus, use direct calls instead if(relay_url === "fetch") { - this.network_adapter = new FetchNetworkAdapter(this.bus); + this.network_adapter = new FetchNetworkAdapter(this.bus, options.net_device); } else if(relay_url.startsWith("wisp://") || relay_url.startsWith("wisps://")) { - this.network_adapter = new WispNetworkAdapter(relay_url, this.bus, options); + this.network_adapter = new WispNetworkAdapter(relay_url, this.bus, options.net_device); } else { diff --git a/src/browser/wisp_network.js b/src/browser/wisp_network.js index 1999e0cdd5..4bb6386449 100644 --- a/src/browser/wisp_network.js +++ b/src/browser/wisp_network.js @@ -3,6 +3,7 @@ /** * @constructor * + * @param {String} wisp_url * @param {BusConnector} bus * @param {*=} config */ @@ -21,10 +22,10 @@ function WispNetworkAdapter(wisp_url, bus, config) this.vm_ip = new Uint8Array((config.vm_ip || "192.168.86.100").split(".").map(function(x) { return parseInt(x, 10); })); this.masquerade = config.masquerade === undefined || !!config.masquerade; this.vm_mac = new Uint8Array(6); + this.dns_method = config.dns_method || "doh"; + this.doh_server = config.doh_server; this.tcp_conn = {}; this.eth_encoder_buf = create_eth_encoder_buf(); - this.dns_method = "doh"; - this.doh_server = config.doh_server; this.bus.register("net" + this.id + "-mac", function(mac) { this.vm_mac = new Uint8Array(mac.split(":").map(function(x) { return parseInt(x, 16); })); From 8f5d58b0298528c242cf05e816ffcd23080fad60 Mon Sep 17 00:00:00 2001 From: SuperMaxusa <41739128+SuperMaxusa@users.noreply.github.com> Date: Mon, 16 Dec 2024 21:19:05 +0200 Subject: [PATCH 67/90] examples: add "Networking between browser windows/tabs using the Broadcast Channel API" (#1196) --- Readme.md | 1 + examples/broadcast-network.html | 59 +++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 examples/broadcast-network.html diff --git a/Readme.md b/Readme.md index b20eddbcb6..cbbc42f9d8 100644 --- a/Readme.md +++ b/Readme.md @@ -168,6 +168,7 @@ See [tests/Readme.md](tests/Readme.md) for more information. - [Programatically using the serial terminal](examples/serial.html) - [A Lua interpreter](examples/lua.html) - [Two instances in one window](examples/two_instances.html) +- [Networking between browser windows/tabs using the Broadcast Channel API](examples/broadcast-network.html) - [Saving and restoring emulator state](examples/save_restore.html) Using v86 for your own purposes is as easy as: diff --git a/examples/broadcast-network.html b/examples/broadcast-network.html new file mode 100644 index 0000000000..c879c9c086 --- /dev/null +++ b/examples/broadcast-network.html @@ -0,0 +1,59 @@ + +Networking via Broadcast Channel API + + + + +
+
+ +
+ +
+# Configure a static IP
+ifconfig eth0 up arp 10.5.0.x
+
+# Ping by IP
+ping 10.5.0.x
+
+# Run a DNS server and send a query (10.5.0.x for server, 10.5.0.y for record)
+echo "anotherhost 10.5.0.y" | dnsd -c - -v    - server
+nslookup -type=a anotherhost 10.5.0.x         - client
+
+# Telnet calculator
+socat TCP-L:23,fork exec:bc
+
+# Simple HTTP server
+socat TCP-L:80,crlf,fork system:'echo HTTP/1.1 200 OK;echo;lua /root/test.lua'
+
From 0612c6cc4b577d6afde5c5df917653193db30381 Mon Sep 17 00:00:00 2001 From: Christian Schnell Date: Fri, 20 Dec 2024 18:24:11 +0100 Subject: [PATCH 68/90] stop reconnecting websocket after NetworkAdapter.destroy() was called (#1198) --- src/browser/network.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/browser/network.js b/src/browser/network.js index 7972c7eb41..22d58f26bb 100644 --- a/src/browser/network.js +++ b/src/browser/network.js @@ -25,6 +25,7 @@ function NetworkAdapter(url, bus, id) this.reconnect_interval = 10000; this.last_connect_attempt = Date.now() - this.reconnect_interval; this.send_queue_limit = 64; + this.destroyed = false; this.bus.register("net" + this.id + "-send", function(data) { @@ -44,8 +45,11 @@ NetworkAdapter.prototype.handle_close = function(e) { //console.log("onclose", e); - this.connect(); - setTimeout(this.connect.bind(this), this.reconnect_interval); + if(!this.destroyed) + { + this.connect(); + setTimeout(this.connect.bind(this), this.reconnect_interval); + } }; NetworkAdapter.prototype.handle_open = function(e) @@ -67,6 +71,7 @@ NetworkAdapter.prototype.handle_error = function(e) NetworkAdapter.prototype.destroy = function() { + this.destroyed = true; if(this.socket) { this.socket.close(); From 14e0897bdbfd320d6430f177ab833c83ff769eac Mon Sep 17 00:00:00 2001 From: Fabian Date: Fri, 15 Nov 2024 11:00:59 -0600 Subject: [PATCH 69/90] fix image download --- src/browser/main.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/browser/main.js b/src/browser/main.js index 15a88736ec..386d14f863 100644 --- a/src/browser/main.js +++ b/src/browser/main.js @@ -1924,7 +1924,7 @@ }, 3000); } - init_ui(settings, emulator); + init_ui(profile, settings, emulator); if(query_args?.has("c")) { @@ -1960,7 +1960,7 @@ * @param {Object} settings * @param {V86} emulator */ - function init_ui(settings, emulator) + function init_ui(profile, settings, emulator) { $("loading").style.display = "none"; $("runtime_options").style.display = "block"; @@ -2199,7 +2199,7 @@ { var elem = $("get_" + type + "_image"); - if(!obj || obj.size > 100 * 1024 * 1024) + if(!obj || obj.async) { elem.style.display = "none"; return; @@ -2207,7 +2207,7 @@ elem.onclick = function(e) { - const filename = buffer.file && buffer.file.name || (settings.id + (type === "cdrom" ? ".iso" : ".img")); + const filename = buffer.file && buffer.file.name || ((profile?.id || "v86") + (type === "cdrom" ? ".iso" : ".img")); if(buffer.get_as_file) { From 71587cbca77bc25bdcc532fdc79d93987be51b64 Mon Sep 17 00:00:00 2001 From: Fabian Date: Mon, 25 Nov 2024 16:39:17 -0600 Subject: [PATCH 70/90] update fiwix --- src/browser/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/main.js b/src/browser/main.js index 386d14f863..a4581cb286 100644 --- a/src/browser/main.js +++ b/src/browser/main.js @@ -259,7 +259,7 @@ id: "fiwix", memory_size: 256 * 1024 * 1024, hda: { - url: host + "FiwixOS-3.3-i386/.img", + url: host + "FiwixOS-3.4-i386/.img", size: 1024 * 1024 * 1024, async: true, fixed_chunk_size: 1024 * 1024, From 24377a2ad5dad3e0e9019f7194e3f03af977bb8e Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 11 Dec 2024 16:48:21 -0700 Subject: [PATCH 71/90] unused no_mangle --- src/rust/cpu/vga.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/rust/cpu/vga.rs b/src/rust/cpu/vga.rs index 84c722cbcd..fa3ad95c0f 100644 --- a/src/rust/cpu/vga.rs +++ b/src/rust/cpu/vga.rs @@ -14,7 +14,6 @@ pub unsafe fn svga_allocate_dest_buffer(size: u32) -> u32 { dest_buffer.as_mut_ptr() as u32 } -#[no_mangle] pub unsafe fn mark_dirty(addr: u32) { let page = (addr - memory::VGA_LFB_ADDRESS) >> 12; dbg_assert!(((page >> 6) as usize) < dirty_bitmap.len()); From 1598f9eb04647821011d52a5ae129a20bdb783bd Mon Sep 17 00:00:00 2001 From: Fabian Date: Mon, 16 Dec 2024 12:21:30 -0700 Subject: [PATCH 72/90] examples/broadcast-network: add description --- examples/broadcast-network.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/broadcast-network.html b/examples/broadcast-network.html index c879c9c086..212891268b 100644 --- a/examples/broadcast-network.html +++ b/examples/broadcast-network.html @@ -41,6 +41,8 @@
+# This example allows network across multiple browser tabs by using BroadcastChannels.
+
 # Configure a static IP
 ifconfig eth0 up arp 10.5.0.x
 

From 9beaccc8baa96a4271e6d091930ee77691c134d1 Mon Sep 17 00:00:00 2001
From: Fabian 
Date: Tue, 17 Dec 2024 20:09:18 -0700
Subject: [PATCH 73/90] pic: fix invalid uses of static mut

---
 src/rust/cpu/cpu.rs |   6 +-
 src/rust/cpu/pic.rs | 357 +++++++++++++++++++++++---------------------
 2 files changed, 191 insertions(+), 172 deletions(-)

diff --git a/src/rust/cpu/cpu.rs b/src/rust/cpu/cpu.rs
index 4bfdc829b2..9a5107af98 100644
--- a/src/rust/cpu/cpu.rs
+++ b/src/rust/cpu/cpu.rs
@@ -4248,9 +4248,11 @@ pub unsafe fn trigger_ss(code: i32) {
 pub unsafe fn store_current_tsc() { *current_tsc = read_tsc(); }
 
 #[no_mangle]
-pub unsafe fn handle_irqs() {
+pub unsafe fn handle_irqs() { handle_irqs_internal(&mut pic::get_pic()) }
+
+pub unsafe fn handle_irqs_internal(pic: &mut pic::Pic) {
     if *flags & FLAG_INTERRUPT != 0 {
-        if let Some(irq) = pic::pic_acknowledge_irq() {
+        if let Some(irq) = pic::pic_acknowledge_irq(pic) {
             pic_call_irq(irq)
         }
         else if *acpi_enabled {
diff --git a/src/rust/cpu/pic.rs b/src/rust/cpu/pic.rs
index 613854dceb..44fd6f8209 100644
--- a/src/rust/cpu/pic.rs
+++ b/src/rust/cpu/pic.rs
@@ -5,12 +5,14 @@
 
 pub const PIC_LOG: bool = false;
 pub const PIC_LOG_VERBOSE: bool = false;
+
 use cpu::cpu;
+use std::sync::{Mutex, MutexGuard};
 
 // Note: This layout is deliberately chosen to match the old JavaScript pic state
 // (cpu.get_state_pic depens on this layout)
 #[repr(C, packed)]
-struct Pic {
+struct Pic0 {
     irq_mask: u8,
 
     irq_map: u8,
@@ -37,53 +39,59 @@ struct Pic {
     special_mask_mode: bool,
 }
 
-#[allow(non_upper_case_globals)]
-static mut master: Pic = Pic {
-    // all irqs off
-    irq_mask: 0,
-    // Bogus default value (both master and slave mapped to 0).
-    // Will be initialized by the BIOS
-    irq_map: 0,
-    // in-service register
-    // Holds interrupts that are currently being serviced
-    isr: 0,
-    // interrupt request register
-    // Holds interrupts that have been requested
-    irr: 0,
-    irq_value: 0,
-    expect_icw4: false,
-    state: 0,
-    read_isr: false,
-    auto_eoi: false,
-    special_mask_mode: false,
-    elcr: 0,
-    master: true,
-    dummy: 0,
-};
-
-#[allow(non_upper_case_globals)]
-static mut slave: Pic = Pic {
-    // all irqs off
-    irq_mask: 0,
-    // Bogus default value (both master and slave mapped to 0).
-    // Will be initialized by the BIOS
-    irq_map: 0,
-    // in-service register
-    // Holds interrupts that are currently being serviced
-    isr: 0,
-    // interrupt request register
-    // Holds interrupts that have been requested
-    irr: 0,
-    irq_value: 0,
-    expect_icw4: false,
-    state: 0,
-    read_isr: false,
-    auto_eoi: false,
-    special_mask_mode: false,
-    elcr: 0,
-    master: false,
-    dummy: 0,
-};
+pub struct Pic {
+    master: Pic0,
+    slave: Pic0,
+}
+
+static PIC: Mutex = Mutex::new(Pic {
+    master: Pic0 {
+        // all irqs off
+        irq_mask: 0,
+        // Bogus default value (both master and slave mapped to 0).
+        // Will be initialized by the BIOS
+        irq_map: 0,
+        // in-service register
+        // Holds interrupts that are currently being serviced
+        isr: 0,
+        // interrupt request register
+        // Holds interrupts that have been requested
+        irr: 0,
+        irq_value: 0,
+        expect_icw4: false,
+        state: 0,
+        read_isr: false,
+        auto_eoi: false,
+        special_mask_mode: false,
+        elcr: 0,
+        master: true,
+        dummy: 0,
+    },
+    slave: Pic0 {
+        // all irqs off
+        irq_mask: 0,
+        // Bogus default value (both master and slave mapped to 0).
+        // Will be initialized by the BIOS
+        irq_map: 0,
+        // in-service register
+        // Holds interrupts that are currently being serviced
+        isr: 0,
+        // interrupt request register
+        // Holds interrupts that have been requested
+        irr: 0,
+        irq_value: 0,
+        expect_icw4: false,
+        state: 0,
+        read_isr: false,
+        auto_eoi: false,
+        special_mask_mode: false,
+        elcr: 0,
+        master: false,
+        dummy: 0,
+    },
+});
+
+pub fn get_pic() -> MutexGuard<'static, Pic> { PIC.try_lock().unwrap() }
 
 // Checking for callable interrupts:
 // (cpu changes interrupt flag) -> cpu.handle_irqs -> pic_acknowledge_irq
@@ -92,7 +100,7 @@ static mut slave: Pic = Pic {
 // triggering irqs:
 // (io device has irq) -> cpu.device_raise_irq -> pic.set_irq -> pic.check_irqs -> cpu.handle_irqs -> (see above)
 
-impl Pic {
+impl Pic0 {
     unsafe fn get_irq(&mut self) -> Option {
         let enabled_irr = self.irr & self.irq_mask;
 
@@ -136,78 +144,73 @@ impl Pic {
         Some(irq_number)
     }
 
-    unsafe fn check_irqs(&mut self) {
-        let is_set = self.get_irq().is_some();
+    unsafe fn port0_read(self: &Pic0) -> u32 {
+        (if self.read_isr { self.isr } else { self.irr }) as u32
+    }
+    unsafe fn port1_read(self: &Pic0) -> u32 { !self.irq_mask as u32 }
+}
 
-        if self.master {
-            if is_set {
-                cpu::handle_irqs();
-            }
-        }
-        else {
-            if is_set {
-                master.set_irq(2)
+impl Pic {
+    unsafe fn set_irq(self: &mut Pic, i: u8) {
+        let mask = 1 << (i & 7);
+        let dev = if i < 8 { &mut self.master } else { &mut self.slave };
+        if dev.irq_value & mask == 0 || dev.elcr & mask != 0 {
+            dev.irr |= mask;
+            dev.irq_value |= mask;
+            if i < 8 {
+                self.check_irqs_master()
             }
             else {
-                master.clear_irq(2)
+                self.check_irqs_slave()
             }
         }
     }
 
-    unsafe fn set_irq(&mut self, i: u8) {
-        let mask = 1 << i;
-        if self.irq_value & mask == 0 || self.elcr & mask != 0 {
-            self.irr |= mask;
-            self.irq_value |= mask;
-            self.check_irqs()
-        }
-    }
-
-    unsafe fn clear_irq(&mut self, i: u8) {
-        let mask = 1 << i;
-        if self.elcr & mask != 0 {
-            self.irq_value &= !mask;
-            self.irr &= !mask;
-            self.check_irqs()
-        }
-        else if self.irq_value & mask != 0 {
-            self.irq_value &= !mask;
-            self.check_irqs()
+    unsafe fn clear_irq(self: &mut Pic, i: u8) {
+        let mask = 1 << (i & 7);
+        let dev = if i < 8 { &mut self.master } else { &mut self.slave };
+        dev.irq_value &= !mask;
+        if dev.elcr & mask != 0 {
+            dev.irr &= !mask;
+            if i < 8 {
+                self.check_irqs_master()
+            }
+            else {
+                self.check_irqs_slave()
+            }
         }
     }
 
-    unsafe fn port0_read(self: &Pic) -> u32 {
-        (if self.read_isr { self.isr } else { self.irr }) as u32
-    }
-    unsafe fn port1_read(self: &Pic) -> u32 { !self.irq_mask as u32 }
-
-    unsafe fn port0_write(&mut self, v: u8) {
+    unsafe fn port0_write(&mut self, index: u8, v: u8) {
+        let dev = if index == 0 { &mut self.master } else { &mut self.slave };
         if v & 0x10 != 0 {
             // xxxx1xxx
             // icw1
             dbg_log!("icw1 = {:x}", v);
-            self.isr = 0;
-            self.irr = 0;
-            self.irq_mask = 0xff;
-            self.irq_value = 0;
-            self.auto_eoi = true;
-
-            self.expect_icw4 = v & 1 != 0;
-            self.state = 1;
+            dev.isr = 0;
+            dev.irr = 0;
+            dev.irq_mask = 0xff;
+            dev.irq_value = 0;
+            dev.auto_eoi = true;
+
+            dev.expect_icw4 = v & 1 != 0;
+            dbg_assert!(v & 2 == 0, "unimplemented: single mode");
+            dbg_assert!(v & 8 == 0, "unimplemented: level mode");
+            dev.state = 1;
         }
         else if v & 8 != 0 {
             // xxx01xxx
             // ocw3
             dbg_log!("ocw3: {:x}", v);
             if v & 2 != 0 {
-                self.read_isr = v & 1 != 0;
+                dev.read_isr = v & 1 != 0;
             }
             if v & 4 != 0 {
                 dbg_assert!(false, "unimplemented: polling");
             }
             if v & 0x40 != 0 {
-                self.special_mask_mode = (v & 0x20) == 0x20;
-                dbg_log!("special mask mode: {}", self.special_mask_mode);
+                dev.special_mask_mode = (v & 0x20) == 0x20;
+                dbg_log!("special mask mode: {}", dev.special_mask_mode);
             }
         }
         else {
@@ -222,76 +225,100 @@ impl Pic {
 
             if eoi_type == 1 {
                 // non-specific eoi
-                self.isr &= self.isr - 1;
+                dev.isr &= dev.isr - 1;
                 if PIC_LOG {
-                    dbg_log!("new isr: {:x}", self.isr);
+                    dbg_log!("new isr: {:x}", dev.isr);
                 }
             }
             else if eoi_type == 3 {
                 // specific eoi
-                self.isr &= !(1 << (v & 7));
+                dev.isr &= !(1 << (v & 7));
             }
-            else if (v & 0xC8) == 0xC0 {
-                // os2 v4
+            else if eoi_type == 6 {
+                // os2 v4, freebsd
                 let priority = v & 7;
                 dbg_log!("lowest priority: {:x}", priority);
             }
             else {
-                dbg_log!("Unknown eoi: {:x}", v);
+                dbg_log!("Unknown eoi: {:x} type={:x}", v, eoi_type);
                 dbg_assert!(false);
-                self.isr &= self.isr - 1;
+                dev.isr &= dev.isr - 1;
             }
 
-            self.check_irqs()
+            if index == 0 {
+                self.check_irqs_master()
+            }
+            else {
+                self.check_irqs_slave()
+            }
         }
     }
 
-    unsafe fn port1_write(&mut self, v: u8) {
-        //dbg_log!("21 write: " + h(v));
-        if self.state == 0 {
-            if self.expect_icw4 {
+    unsafe fn port1_write(&mut self, index: u8, v: u8) {
+        let dev = if index == 0 { &mut self.master } else { &mut self.slave };
+        if dev.state == 0 {
+            if dev.expect_icw4 {
                 // icw4
-                self.expect_icw4 = false;
-                self.auto_eoi = v & 2 != 0;
-                dbg_log!("icw4: {:x} autoeoi={}", v, self.auto_eoi);
-
-                if v & 1 == 0 {
-                    dbg_assert!(false, "unimplemented: not 8086 mode");
-                }
+                dev.expect_icw4 = false;
+                dev.auto_eoi = v & 2 != 0;
+                dbg_log!("icw4: {:x} autoeoi={}", v, dev.auto_eoi);
+                dbg_assert!(v & 0x10 == 0, "unimplemented: nested mode");
+                dbg_assert!(v & 1 == 1, "unimplemented: 8086/88 mode");
             }
             else {
                 // ocw1
-                self.irq_mask = !v;
+                dev.irq_mask = !v;
 
                 if PIC_LOG_VERBOSE {
-                    dbg_log!("interrupt mask: {:x}", self.irq_mask);
+                    dbg_log!("interrupt mask: {:x}", dev.irq_mask);
                 }
 
-                self.check_irqs()
+                if index == 0 {
+                    self.check_irqs_master()
+                }
+                else {
+                    self.check_irqs_slave()
+                }
             }
         }
-        else if self.state == 1 {
+        else if dev.state == 1 {
             // icw2
-            self.irq_map = v;
-            dbg_log!("interrupts are mapped to {:x}", self.irq_map);
-            self.state += 1;
+            dev.irq_map = v;
+            dbg_log!("interrupts are mapped to {:x}", dev.irq_map);
+            dev.state += 1;
         }
-        else if self.state == 2 {
+        else if dev.state == 2 {
             // icw3
-            self.state = 0;
+            dev.state = 0;
             dbg_log!("icw3: {:x}", v);
         }
     }
+
+    unsafe fn check_irqs_master(&mut self) {
+        let is_set = self.master.get_irq().is_some();
+        if is_set {
+            cpu::handle_irqs_internal(self);
+        }
+    }
+    unsafe fn check_irqs_slave(&mut self) {
+        let is_set = self.slave.get_irq().is_some();
+        if is_set {
+            self.set_irq(2)
+        }
+        else {
+            self.clear_irq(2)
+        }
+    }
 }
 
 // called by the cpu
-pub unsafe fn pic_acknowledge_irq() -> Option {
-    let irq = match master.get_irq() {
+pub unsafe fn pic_acknowledge_irq(pic: &mut Pic) -> Option {
+    let irq = match pic.master.get_irq() {
         Some(i) => i,
         None => return None,
     };
 
-    if master.irr == 0 {
+    if pic.master.irr == 0 {
         dbg_assert!(false);
         //PIC_LOG_VERBOSE && dbg_log!("master> spurious requested=" + irq);
         //Some(pic.irq_map | 7)
@@ -300,36 +327,36 @@ pub unsafe fn pic_acknowledge_irq() -> Option {
 
     let mask = 1 << irq;
 
-    if master.elcr & mask == 0 {
+    if pic.master.elcr & mask == 0 {
         // not in level mode
-        master.irr &= !mask;
+        pic.master.irr &= !mask;
     }
 
-    if !master.auto_eoi {
-        master.isr |= mask;
+    if !pic.master.auto_eoi {
+        pic.master.isr |= mask;
     }
 
     if PIC_LOG_VERBOSE {
         dbg_log!("[PIC] master> acknowledge {}", irq);
     }
 
-    master.check_irqs();
+    //pic.check_irqs_master(); // XXX
 
     if irq == 2 {
-        acknowledge_irq_slave()
+        acknowledge_irq_slave(pic)
     }
     else {
-        Some(master.irq_map | irq)
+        Some(pic.master.irq_map | irq)
     }
 }
 
-unsafe fn acknowledge_irq_slave() -> Option {
-    let irq = match slave.get_irq() {
+unsafe fn acknowledge_irq_slave(pic: &mut Pic) -> Option {
+    let irq = match pic.slave.get_irq() {
         Some(i) => i,
         None => return None,
     };
 
-    if slave.irr == 0 {
+    if pic.slave.irr == 0 {
         //PIC_LOG_VERBOSE && dbg_log!("slave> spurious requested=" + irq);
         //Some(pic.irq_map | 7)
         dbg_assert!(false);
@@ -338,21 +365,21 @@ unsafe fn acknowledge_irq_slave() -> Option {
 
     let mask = 1 << irq;
 
-    if slave.elcr & mask == 0 {
+    if pic.slave.elcr & mask == 0 {
         // not in level mode
-        slave.irr &= !mask;
+        pic.slave.irr &= !mask;
     }
 
-    if !slave.auto_eoi {
-        slave.isr |= mask;
+    if !pic.slave.auto_eoi {
+        pic.slave.isr |= mask;
     }
 
     if PIC_LOG_VERBOSE {
         dbg_log!("[PIC] slave> acknowledge {}", irq);
     }
-    slave.check_irqs();
+    //pic.check_irqs_slave(); // XXX
 
-    Some(slave.irq_map | irq)
+    Some(pic.slave.irq_map | irq)
 }
 
 // called by javascript
@@ -361,15 +388,10 @@ pub unsafe fn pic_set_irq(i: u8) {
     dbg_assert!(i < 16);
 
     if PIC_LOG_VERBOSE {
-        dbg_log!("[PIC] set irq {}, irq_value={:x}", i, master.irq_value);
+        dbg_log!("[PIC] set irq {}", i);
     }
 
-    if i < 8 {
-        master.set_irq(i)
-    }
-    else {
-        slave.set_irq(i - 8)
-    }
+    get_pic().set_irq(i)
 }
 
 // called by javascript
@@ -381,44 +403,39 @@ pub unsafe fn pic_clear_irq(i: u8) {
         dbg_log!("[PIC] clear irq {}", i);
     }
 
-    if i < 8 {
-        master.clear_irq(i)
-    }
-    else {
-        slave.clear_irq(i - 8)
-    }
+    get_pic().clear_irq(i)
 }
 
 #[no_mangle]
-pub unsafe fn port20_read() -> u32 { master.port0_read() }
+pub unsafe fn port20_read() -> u32 { get_pic().master.port0_read() }
 #[no_mangle]
-pub unsafe fn port21_read() -> u32 { master.port1_read() }
+pub unsafe fn port21_read() -> u32 { get_pic().master.port1_read() }
 
 #[no_mangle]
-pub unsafe fn portA0_read() -> u32 { slave.port0_read() }
+pub unsafe fn portA0_read() -> u32 { get_pic().slave.port0_read() }
 #[no_mangle]
-pub unsafe fn portA1_read() -> u32 { slave.port1_read() }
+pub unsafe fn portA1_read() -> u32 { get_pic().slave.port1_read() }
 
 #[no_mangle]
-pub unsafe fn port20_write(v: u8) { master.port0_write(v) }
+pub unsafe fn port20_write(v: u8) { get_pic().port0_write(0, v) }
 #[no_mangle]
-pub unsafe fn port21_write(v: u8) { master.port1_write(v) }
+pub unsafe fn port21_write(v: u8) { get_pic().port1_write(0, v) }
 
 #[no_mangle]
-pub unsafe fn portA0_write(v: u8) { slave.port0_write(v) }
+pub unsafe fn portA0_write(v: u8) { get_pic().port0_write(1, v) }
 #[no_mangle]
-pub unsafe fn portA1_write(v: u8) { slave.port1_write(v) }
+pub unsafe fn portA1_write(v: u8) { get_pic().port1_write(1, v) }
 
 #[no_mangle]
-pub unsafe fn port4D0_read() -> u32 { master.elcr as u32 }
+pub unsafe fn port4D0_read() -> u32 { get_pic().master.elcr as u32 }
 #[no_mangle]
-pub unsafe fn port4D1_read() -> u32 { slave.elcr as u32 }
+pub unsafe fn port4D1_read() -> u32 { get_pic().slave.elcr as u32 }
 #[no_mangle]
-pub unsafe fn port4D0_write(v: u8) { master.elcr = v }
+pub unsafe fn port4D0_write(v: u8) { get_pic().master.elcr = v }
 #[no_mangle]
-pub unsafe fn port4D1_write(v: u8) { slave.elcr = v }
+pub unsafe fn port4D1_write(v: u8) { get_pic().slave.elcr = v }
 
 #[no_mangle]
-pub unsafe fn get_pic_addr_master() -> u32 { std::ptr::addr_of_mut!(master) as u32 }
+pub unsafe fn get_pic_addr_master() -> u32 { &raw const get_pic().master as u32 }
 #[no_mangle]
-pub unsafe fn get_pic_addr_slave() -> u32 { std::ptr::addr_of_mut!(slave) as u32 }
+pub unsafe fn get_pic_addr_slave() -> u32 { &raw const get_pic().slave as u32 }

From b5edf12949451cb2957e78319199a4034e709c5f Mon Sep 17 00:00:00 2001
From: Fabian 
Date: Thu, 19 Dec 2024 19:14:30 -0700
Subject: [PATCH 74/90] fix some uses of static mut (replace .as_mut_ptr()) by
 &raw mut

---
 src/rust/codegen.rs | 2 +-
 src/rust/cpu/cpu.rs | 8 ++++----
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/rust/codegen.rs b/src/rust/codegen.rs
index aebf46edb1..cba10318ef 100644
--- a/src/rust/codegen.rs
+++ b/src/rust/codegen.rs
@@ -2648,7 +2648,7 @@ pub fn gen_profiler_stat_increment(builder: &mut WasmBuilder, stat: profiler::st
     if !cfg!(feature = "profiler") {
         return;
     }
-    let addr = unsafe { profiler::stat_array.as_mut_ptr().offset(stat as isize) } as u32;
+    let addr = unsafe { &raw mut profiler::stat_array[stat as usize] } as u32;
     builder.increment_fixed_i64(addr, 1)
 }
 
diff --git a/src/rust/cpu/cpu.rs b/src/rust/cpu/cpu.rs
index 9a5107af98..116d89dde4 100644
--- a/src/rust/cpu/cpu.rs
+++ b/src/rust/cpu/cpu.rs
@@ -3412,7 +3412,7 @@ pub unsafe fn safe_read_slow_jit(
         // TODO: Could check if virtual pages point to consecutive physical and go to fast path
         // do read, write into scratch buffer
 
-        let scratch = jit_paging_scratch_buffer.0.as_mut_ptr() as u32;
+        let scratch = &raw mut jit_paging_scratch_buffer.0 as u32;
         dbg_assert!(scratch & 0xFFF == 0);
 
         for s in addr_low..((addr_low | 0xFFF) + 1) {
@@ -3425,7 +3425,7 @@ pub unsafe fn safe_read_slow_jit(
         ((scratch as i32) ^ addr) & !0xFFF
     }
     else if in_mapped_range(addr_low) {
-        let scratch = jit_paging_scratch_buffer.0.as_mut_ptr();
+        let scratch = &raw mut jit_paging_scratch_buffer.0[0];
 
         match bitsize {
             128 => ptr::write_unaligned(
@@ -3571,7 +3571,7 @@ pub unsafe fn safe_write_slow_jit(
             },
         }
 
-        let scratch = jit_paging_scratch_buffer.0.as_mut_ptr() as u32;
+        let scratch = &raw mut jit_paging_scratch_buffer.0 as u32;
         dbg_assert!(scratch & 0xFFF == 0);
         ((scratch as i32) ^ addr) & !0xFFF
     }
@@ -3587,7 +3587,7 @@ pub unsafe fn safe_write_slow_jit(
             },
         }
 
-        let scratch = jit_paging_scratch_buffer.0.as_mut_ptr() as u32;
+        let scratch = &raw mut jit_paging_scratch_buffer.0 as u32;
         dbg_assert!(scratch & 0xFFF == 0);
         ((scratch as i32) ^ addr) & !0xFFF
     }

From 253e9507c819213b681a8652a14f49adad677fbd Mon Sep 17 00:00:00 2001
From: Fabian 
Date: Thu, 19 Dec 2024 19:20:16 -0700
Subject: [PATCH 75/90] allow static_mut_refs where reasoning about them is
 easy

---
 src/rust/cpu/cpu.rs    |  8 ++++++--
 src/rust/cpu/memory.rs |  8 ++++----
 src/rust/cpu/vga.rs    | 11 ++++++++---
 src/rust/jit.rs        |  2 ++
 src/rust/profiler.rs   |  1 +
 5 files changed, 21 insertions(+), 9 deletions(-)

diff --git a/src/rust/cpu/cpu.rs b/src/rust/cpu/cpu.rs
index 116d89dde4..edfb910011 100644
--- a/src/rust/cpu/cpu.rs
+++ b/src/rust/cpu/cpu.rs
@@ -2113,6 +2113,7 @@ pub unsafe fn full_clear_tlb() {
     valid_tlb_entries_count = 0;
 
     if CHECK_TLB_INVARIANTS {
+        #[allow(static_mut_refs)]
         for &entry in tlb_data.iter() {
             dbg_assert!(entry == 0);
         }
@@ -2141,6 +2142,7 @@ pub unsafe fn clear_tlb() {
     valid_tlb_entries_count = global_page_offset;
 
     if CHECK_TLB_INVARIANTS {
+        #[allow(static_mut_refs)]
         for &entry in tlb_data.iter() {
             dbg_assert!(entry == 0 || 0 != entry & TLB_GLOBAL);
         }
@@ -2181,6 +2183,7 @@ pub unsafe fn trigger_gp_jit(code: i32, eip_offset_in_page: i32) {
 
 #[no_mangle]
 pub unsafe fn trigger_fault_end_jit() {
+    #[allow(static_mut_refs)]
     let (code, error_code) = jit_fault.take().unwrap();
     if DEBUG {
         if cpu_exception_hook(code) {
@@ -2924,11 +2927,12 @@ pub unsafe fn cycle_internal() {
         );
 
         if cfg!(feature = "profiler") {
-            dbg_assert!(match ::cpu::cpu::debug_last_jump {
+            dbg_assert!(match debug_last_jump {
                 LastJump::Compiled { .. } => true,
                 _ => false,
             });
-            let last_jump_addr = ::cpu::cpu::debug_last_jump.phys_address().unwrap();
+            #[allow(static_mut_refs)]
+            let last_jump_addr = debug_last_jump.phys_address().unwrap();
             let last_jump_opcode = if last_jump_addr != 0 {
                 read32s(last_jump_addr)
             }
diff --git a/src/rust/cpu/memory.rs b/src/rust/cpu/memory.rs
index 76c343b3b3..932a8296cd 100644
--- a/src/rust/cpu/memory.rs
+++ b/src/rust/cpu/memory.rs
@@ -51,17 +51,17 @@ pub fn svga_allocate_memory(size: u32) -> u32 {
         dbg_assert!(vga_mem8.is_null());
     };
     let layout = alloc::Layout::from_size_align(size as usize, 0x1000).unwrap();
-    let ptr = unsafe { alloc::alloc(layout) as u32 };
+    let ptr = unsafe { alloc::alloc(layout) };
     dbg_assert!(
         size & (1 << 12 << 6) == 0,
         "size not aligned to dirty_bitmap"
     );
     unsafe {
-        vga_mem8 = ptr as *mut u8;
+        vga_mem8 = ptr;
         vga_memory_size = size;
-        vga::dirty_bitmap.resize((size >> 12 >> 6) as usize, 0);
+        vga::set_dirty_bitmap_size(size >> 12 >> 6);
     };
-    ptr
+    ptr as u32
 }
 
 #[no_mangle]
diff --git a/src/rust/cpu/vga.rs b/src/rust/cpu/vga.rs
index fa3ad95c0f..976abca0c6 100644
--- a/src/rust/cpu/vga.rs
+++ b/src/rust/cpu/vga.rs
@@ -1,19 +1,24 @@
 #![allow(non_upper_case_globals)]
+#![allow(static_mut_refs)]
+
+// Safety of allow(static_mut_refs) in this file:
+// These following two globals are not passed anywhere, only built-in function are called on them
+static mut dirty_bitmap: Vec = Vec::new();
+static mut dest_buffer: Vec = Vec::new();
 
 use cpu::global_pointers;
 use cpu::memory;
 
 use std::ptr;
 
-pub static mut dirty_bitmap: Vec = Vec::new();
-pub static mut dest_buffer: Vec = Vec::new();
-
 #[no_mangle]
 pub unsafe fn svga_allocate_dest_buffer(size: u32) -> u32 {
     dest_buffer.resize(size as usize, 0);
     dest_buffer.as_mut_ptr() as u32
 }
 
+pub unsafe fn set_dirty_bitmap_size(size: u32) { dirty_bitmap.resize(size as usize, 0); }
+
 pub unsafe fn mark_dirty(addr: u32) {
     let page = (addr - memory::VGA_LFB_ADDRESS) >> 12;
     dbg_assert!(((page >> 6) as usize) < dirty_bitmap.len());
diff --git a/src/rust/jit.rs b/src/rust/jit.rs
index b1d782b331..1cab738183 100644
--- a/src/rust/jit.rs
+++ b/src/rust/jit.rs
@@ -2347,7 +2347,9 @@ pub fn check_missed_entry_points(phys_address: u32, state_flags: CachedStateFlag
             return;
         }
 
+        #[allow(static_mut_refs)]
         let last_jump_type = unsafe { cpu::debug_last_jump.name() };
+        #[allow(static_mut_refs)]
         let last_jump_addr = unsafe { cpu::debug_last_jump.phys_address() }.unwrap_or(0);
         let last_jump_opcode =
             if last_jump_addr != 0 { memory::read32s(last_jump_addr) } else { 0 };
diff --git a/src/rust/profiler.rs b/src/rust/profiler.rs
index 50ed4211fd..4c8d0f4b43 100644
--- a/src/rust/profiler.rs
+++ b/src/rust/profiler.rs
@@ -134,6 +134,7 @@ pub fn stat_increment_by(stat: stat, by: u64) {
 #[no_mangle]
 pub fn profiler_init() {
     unsafe {
+        #[allow(static_mut_refs)]
         for x in stat_array.iter_mut() {
             *x = 0
         }

From 5a20f9de01941868f60d6d2abafbdc8e60823670 Mon Sep 17 00:00:00 2001
From: Fabian 
Date: Thu, 19 Dec 2024 19:21:16 -0700
Subject: [PATCH 76/90] minor: fix static_mut_refs when printing u64

---
 src/rust/cpu/cpu.rs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/rust/cpu/cpu.rs b/src/rust/cpu/cpu.rs
index edfb910011..2ebfcaaca2 100644
--- a/src/rust/cpu/cpu.rs
+++ b/src/rust/cpu/cpu.rs
@@ -4068,8 +4068,8 @@ pub unsafe fn read_tsc() -> u64 {
         if TSC_VERBOSE_LOGGING || tsc_last_extra >= tsc_resolution {
             dbg_log!(
                 "rdtsc: jump from {}+{} to {} (diff {}, {}%)",
-                tsc_last_value,
-                tsc_last_extra,
+                tsc_last_value as u64,
+                tsc_last_extra as u64,
                 value,
                 value - (tsc_last_value + tsc_last_extra),
                 (100 * tsc_last_extra) / tsc_resolution,

From 5d867bd592f299a1d83a5f65596f520d126c30e7 Mon Sep 17 00:00:00 2001
From: Fabian 
Date: Thu, 19 Dec 2024 19:22:43 -0700
Subject: [PATCH 77/90] wrap JitState in Mutex and fix an incorrect use

---
 src/rust/cpu/cpu.rs    | 36 ++++++++++++---------
 src/rust/cpu/memory.rs |  7 +++--
 src/rust/cpu/string.rs |  3 +-
 src/rust/jit.rs        | 71 +++++++++++++++++++++++++-----------------
 4 files changed, 71 insertions(+), 46 deletions(-)

diff --git a/src/rust/cpu/cpu.rs b/src/rust/cpu/cpu.rs
index 2ebfcaaca2..ec33eabc9b 100644
--- a/src/rust/cpu/cpu.rs
+++ b/src/rust/cpu/cpu.rs
@@ -2073,7 +2073,15 @@ pub unsafe fn do_page_walk(
     }
 
     let is_in_mapped_range = in_mapped_range(high);
-    let has_code = !is_in_mapped_range && jit::jit_page_has_code(Page::page_of(high));
+    let has_code = if side_effects {
+        !is_in_mapped_range && jit::jit_page_has_code(Page::page_of(high))
+    }
+    else {
+        // If side_effects is false, don't call into jit::jit_page_has_code. This value is not used
+        // anyway (we only get here by translate_address_read_no_side_effects, which only uses the
+        // address part)
+        true
+    };
     let info_bits = TLB_VALID
         | if for_writing { 0 } else { TLB_READONLY }
         | if allow_user { 0 } else { TLB_NO_USER }
@@ -3597,7 +3605,7 @@ pub unsafe fn safe_write_slow_jit(
     }
     else {
         if !can_skip_dirty_page {
-            jit::jit_dirty_page(jit::get_jit_state(), Page::page_of(addr_low));
+            jit::jit_dirty_page(Page::page_of(addr_low));
         }
         ((addr_low as i32 + memory::mem8 as i32) ^ addr) & !0xFFF
     }
@@ -3636,7 +3644,7 @@ pub unsafe fn safe_write8(addr: i32, value: i32) -> OrPageFault<()> {
     }
     else {
         if !can_skip_dirty_page {
-            jit::jit_dirty_page(jit::get_jit_state(), Page::page_of(phys_addr));
+            jit::jit_dirty_page(Page::page_of(phys_addr));
         }
         else {
             dbg_assert!(!jit::jit_page_has_code(Page::page_of(phys_addr as u32)));
@@ -3656,7 +3664,7 @@ pub unsafe fn safe_write16(addr: i32, value: i32) -> OrPageFault<()> {
     }
     else {
         if !can_skip_dirty_page {
-            jit::jit_dirty_page(jit::get_jit_state(), Page::page_of(phys_addr));
+            jit::jit_dirty_page(Page::page_of(phys_addr));
         }
         else {
             dbg_assert!(!jit::jit_page_has_code(Page::page_of(phys_addr as u32)));
@@ -3680,7 +3688,7 @@ pub unsafe fn safe_write32(addr: i32, value: i32) -> OrPageFault<()> {
     }
     else {
         if !can_skip_dirty_page {
-            jit::jit_dirty_page(jit::get_jit_state(), Page::page_of(phys_addr));
+            jit::jit_dirty_page(Page::page_of(phys_addr));
         }
         else {
             dbg_assert!(!jit::jit_page_has_code(Page::page_of(phys_addr as u32)));
@@ -3703,7 +3711,7 @@ pub unsafe fn safe_write64(addr: i32, value: u64) -> OrPageFault<()> {
         }
         else {
             if !can_skip_dirty_page {
-                jit::jit_dirty_page(jit::get_jit_state(), Page::page_of(phys_addr));
+                jit::jit_dirty_page(Page::page_of(phys_addr));
             }
             else {
                 dbg_assert!(!jit::jit_page_has_code(Page::page_of(phys_addr as u32)));
@@ -3727,7 +3735,7 @@ pub unsafe fn safe_write128(addr: i32, value: reg128) -> OrPageFault<()> {
         }
         else {
             if !can_skip_dirty_page {
-                jit::jit_dirty_page(jit::get_jit_state(), Page::page_of(phys_addr));
+                jit::jit_dirty_page(Page::page_of(phys_addr));
             }
             else {
                 dbg_assert!(!jit::jit_page_has_code(Page::page_of(phys_addr as u32)));
@@ -3749,10 +3757,10 @@ pub unsafe fn safe_read_write8(addr: i32, instruction: &dyn Fn(i32) -> i32) {
     }
     else {
         if !can_skip_dirty_page {
-            ::jit::jit_dirty_page(::jit::get_jit_state(), Page::page_of(phys_addr));
+            jit::jit_dirty_page(Page::page_of(phys_addr));
         }
         else {
-            dbg_assert!(!::jit::jit_page_has_code(Page::page_of(phys_addr as u32)));
+            dbg_assert!(!jit::jit_page_has_code(Page::page_of(phys_addr as u32)));
         }
         memory::write8_no_mmap_or_dirty_check(phys_addr, value);
     }
@@ -3775,10 +3783,10 @@ pub unsafe fn safe_read_write16(addr: i32, instruction: &dyn Fn(i32) -> i32) {
         }
         else {
             if !can_skip_dirty_page {
-                ::jit::jit_dirty_page(::jit::get_jit_state(), Page::page_of(phys_addr));
+                jit::jit_dirty_page(Page::page_of(phys_addr));
             }
             else {
-                dbg_assert!(!::jit::jit_page_has_code(Page::page_of(phys_addr as u32)));
+                dbg_assert!(!jit::jit_page_has_code(Page::page_of(phys_addr as u32)));
             }
             memory::write16_no_mmap_or_dirty_check(phys_addr, value);
         };
@@ -3803,10 +3811,10 @@ pub unsafe fn safe_read_write32(addr: i32, instruction: &dyn Fn(i32) -> i32) {
         }
         else {
             if !can_skip_dirty_page {
-                ::jit::jit_dirty_page(::jit::get_jit_state(), Page::page_of(phys_addr));
+                jit::jit_dirty_page(Page::page_of(phys_addr));
             }
             else {
-                dbg_assert!(!::jit::jit_page_has_code(Page::page_of(phys_addr as u32)));
+                dbg_assert!(!jit::jit_page_has_code(Page::page_of(phys_addr as u32)));
             }
             memory::write32_no_mmap_or_dirty_check(phys_addr, value);
         };
@@ -4381,7 +4389,7 @@ pub unsafe fn reset_cpu() {
 
     update_state_flags();
 
-    jit::jit_clear_cache(jit::get_jit_state());
+    jit::jit_clear_cache_js();
 }
 
 #[no_mangle]
diff --git a/src/rust/cpu/memory.rs b/src/rust/cpu/memory.rs
index 932a8296cd..9141c4ebb5 100644
--- a/src/rust/cpu/memory.rs
+++ b/src/rust/cpu/memory.rs
@@ -15,6 +15,7 @@ mod ext {
 use cpu::cpu::reg128;
 use cpu::global_pointers::memory_size;
 use cpu::vga;
+use jit;
 use page::Page;
 
 use std::alloc;
@@ -172,7 +173,7 @@ pub unsafe fn write8(addr: u32, value: i32) {
         mmap_write8(addr, value & 0xFF);
     }
     else {
-        ::jit::jit_dirty_page(::jit::get_jit_state(), Page::page_of(addr));
+        jit::jit_dirty_page(Page::page_of(addr));
         write8_no_mmap_or_dirty_check(addr, value);
     };
 }
@@ -187,7 +188,7 @@ pub unsafe fn write16(addr: u32, value: i32) {
         mmap_write16(addr, value & 0xFFFF);
     }
     else {
-        ::jit::jit_dirty_cache_small(addr, addr + 2);
+        jit::jit_dirty_cache_small(addr, addr + 2);
         write16_no_mmap_or_dirty_check(addr, value);
     };
 }
@@ -201,7 +202,7 @@ pub unsafe fn write32(addr: u32, value: i32) {
         mmap_write32(addr, value);
     }
     else {
-        ::jit::jit_dirty_cache_small(addr, addr + 4);
+        jit::jit_dirty_cache_small(addr, addr + 4);
         write32_no_mmap_or_dirty_check(addr, value);
     };
 }
diff --git a/src/rust/cpu/string.rs b/src/rust/cpu/string.rs
index 0f1500a127..ba639a6c76 100644
--- a/src/rust/cpu/string.rs
+++ b/src/rust/cpu/string.rs
@@ -23,6 +23,7 @@ use cpu::memory::{
     memset_no_mmap_or_dirty_check, read16_no_mmap_check, read32_no_mmap_check, read8_no_mmap_check,
     write16_no_mmap_or_dirty_check, write32_no_mmap_or_dirty_check, write8_no_mmap_or_dirty_check,
 };
+use jit;
 use page::Page;
 
 fn count_until_end_of_page(direction: i32, size: i32, addr: u32) -> u32 {
@@ -248,7 +249,7 @@ unsafe fn string_instruction(
         dbg_assert!(count_until_end_of_page > 0);
 
         if !skip_dirty_page {
-            ::jit::jit_dirty_page(::jit::get_jit_state(), Page::page_of(phys_dst));
+            jit::jit_dirty_page(Page::page_of(phys_dst));
         }
 
         let mut rep_cmp_finished = false;
diff --git a/src/rust/jit.rs b/src/rust/jit.rs
index 1cab738183..0aabee5e92 100644
--- a/src/rust/jit.rs
+++ b/src/rust/jit.rs
@@ -1,7 +1,9 @@
 use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
 use std::iter::FromIterator;
-use std::mem;
+use std::mem::{self, MaybeUninit};
+use std::ops::{Deref, DerefMut};
 use std::ptr::NonNull;
+use std::sync::{Mutex, MutexGuard};
 
 use analysis::AnalysisType;
 use codegen;
@@ -82,18 +84,27 @@ pub const CHECK_JIT_STATE_INVARIANTS: bool = false;
 
 const MAX_INSTRUCTION_LENGTH: u32 = 16;
 
-#[allow(non_upper_case_globals)]
-static mut jit_state: NonNull =
-    unsafe { NonNull::new_unchecked(mem::align_of::() as *mut _) };
+static JIT_STATE: Mutex> = Mutex::new(MaybeUninit::uninit());
+fn get_jit_state() -> JitStateRef { JitStateRef(JIT_STATE.try_lock().unwrap()) }
 
-pub fn get_jit_state() -> &'static mut JitState { unsafe { jit_state.as_mut() } }
+struct JitStateRef(MutexGuard<'static, MaybeUninit>);
+
+impl Deref for JitStateRef {
+    type Target = JitState;
+    fn deref(&self) -> &Self::Target { unsafe { self.0.assume_init_ref() } }
+}
+impl DerefMut for JitStateRef {
+    fn deref_mut(&mut self) -> &mut Self::Target { unsafe { self.0.assume_init_mut() } }
+}
 
 #[no_mangle]
 pub fn rust_init() {
     dbg_assert!(std::mem::size_of::<[Option>; 0x100000]>() == 0x100000 * 4);
 
-    let x = Box::new(JitState::create_and_initialise());
-    unsafe { jit_state = NonNull::new(Box::into_raw(x)).unwrap() }
+    let _ = JIT_STATE
+        .try_lock()
+        .unwrap()
+        .write(JitState::create_and_initialise());
 
     use std::panic;
 
@@ -114,7 +125,7 @@ enum CompilingPageState {
     CompilingWritten,
 }
 
-pub struct JitState {
+struct JitState {
     wasm_builder: WasmBuilder,
 
     // as an alternative to HashSet, we could use a bitmap of 4096 bits here
@@ -127,7 +138,7 @@ pub struct JitState {
     compiling: Option<(WasmTableIndex, CompilingPageState)>,
 }
 
-pub fn check_jit_state_invariants(ctx: &mut JitState) {
+fn check_jit_state_invariants(ctx: &mut JitState) {
     if !CHECK_JIT_STATE_INVARIANTS {
         return;
     }
@@ -1011,7 +1022,7 @@ pub fn codegen_finalize_finished(
     phys_addr: u32,
     state_flags: CachedStateFlags,
 ) {
-    let ctx = get_jit_state();
+    let mut ctx = get_jit_state();
 
     dbg_assert!(wasm_table_index != WasmTableIndex(0));
 
@@ -1029,8 +1040,8 @@ pub fn codegen_finalize_finished(
             dbg_assert!(wasm_table_index == in_progress_wasm_table_index);
 
             profiler::stat_increment(stat::INVALIDATE_MODULE_WRITTEN_WHILE_COMPILED);
-            free_wasm_table_index(ctx, wasm_table_index);
-            check_jit_state_invariants(ctx);
+            free_wasm_table_index(&mut ctx, wasm_table_index);
+            check_jit_state_invariants(&mut ctx);
             return;
         },
         Some((in_progress_wasm_table_index, CompilingPageState::Compiling { pages })) => {
@@ -1083,10 +1094,10 @@ pub fn codegen_finalize_finished(
 
         dbg_log!("unused after overwrite {}", index.to_u16());
         profiler::stat_increment(stat::INVALIDATE_MODULE_UNUSED_AFTER_OVERWRITE);
-        free_wasm_table_index(ctx, index);
+        free_wasm_table_index(&mut ctx, index);
     }
 
-    check_jit_state_invariants(ctx);
+    check_jit_state_invariants(&mut ctx);
 }
 
 pub fn update_tlb_code(virt_page: Page, phys_page: Page) {
@@ -2090,7 +2101,8 @@ pub fn jit_increase_hotness_and_maybe_compile(
         return;
     }
 
-    let ctx = get_jit_state();
+    let mut ctx = get_jit_state();
+    let is_compiling = ctx.compiling.is_some();
     let page = Page::page_of(phys_address);
     let (hotness, entry_points) = ctx.entry_points.entry(page).or_insert_with(|| {
         cpu::tlb_set_has_code(page, true);
@@ -2104,18 +2116,18 @@ pub fn jit_increase_hotness_and_maybe_compile(
 
     *hotness += heat;
     if *hotness >= JIT_THRESHOLD {
-        if ctx.compiling.is_some() {
+        if is_compiling {
             return;
         }
         // only try generating if we're in the correct address space
         if cpu::translate_address_read_no_side_effects(virt_address) == Ok(phys_address) {
             *hotness = 0;
-            jit_analyze_and_generate(ctx, virt_address, phys_address, cs_offset, state_flags)
+            jit_analyze_and_generate(&mut ctx, virt_address, phys_address, cs_offset, state_flags)
         }
         else {
             profiler::stat_increment(stat::COMPILE_WRONG_ADDRESS_SPACE);
         }
-    };
+    }
 }
 
 fn free_wasm_table_index(ctx: &mut JitState, wasm_table_index: WasmTableIndex) {
@@ -2164,7 +2176,7 @@ fn free_wasm_table_index(ctx: &mut JitState, wasm_table_index: WasmTableIndex) {
 }
 
 /// Register a write in this page: Delete all present code
-pub fn jit_dirty_page(ctx: &mut JitState, page: Page) {
+fn jit_dirty_page_ctx(ctx: &mut JitState, page: Page) {
     let mut did_have_code = false;
 
     if let Some(PageInfo {
@@ -2269,10 +2281,13 @@ pub fn jit_dirty_cache(start_addr: u32, end_addr: u32) {
     let end_page = Page::page_of(end_addr - 1);
 
     for page in start_page.to_u32()..end_page.to_u32() + 1 {
-        jit_dirty_page(get_jit_state(), Page::page_of(page << 12));
+        jit_dirty_page_ctx(&mut get_jit_state(), Page::page_of(page << 12));
     }
 }
 
+#[no_mangle]
+pub fn jit_dirty_page(page: Page) { jit_dirty_page_ctx(&mut get_jit_state(), page) }
+
 /// dirty pages in the range of start_addr and end_addr, which must span at most two pages
 pub fn jit_dirty_cache_small(start_addr: u32, end_addr: u32) {
     dbg_assert!(start_addr < end_addr);
@@ -2280,21 +2295,21 @@ pub fn jit_dirty_cache_small(start_addr: u32, end_addr: u32) {
     let start_page = Page::page_of(start_addr);
     let end_page = Page::page_of(end_addr - 1);
 
-    let ctx = get_jit_state();
-    jit_dirty_page(ctx, start_page);
+    let mut ctx = get_jit_state();
+    jit_dirty_page_ctx(&mut ctx, start_page);
 
     // Note: This can't happen when paging is enabled, as writes across
     //       boundaries are split up on two pages
     if start_page != end_page {
         dbg_assert!(start_page.to_u32() + 1 == end_page.to_u32());
-        jit_dirty_page(ctx, end_page);
+        jit_dirty_page_ctx(&mut ctx, end_page);
     }
 }
 
 #[no_mangle]
-pub fn jit_clear_cache_js() { jit_clear_cache(get_jit_state()) }
+pub fn jit_clear_cache_js() { jit_clear_cache(&mut get_jit_state()) }
 
-pub fn jit_clear_cache(ctx: &mut JitState) {
+fn jit_clear_cache(ctx: &mut JitState) {
     let mut pages_with_code = HashSet::new();
 
     for &p in ctx.entry_points.keys() {
@@ -2305,13 +2320,13 @@ pub fn jit_clear_cache(ctx: &mut JitState) {
     }
 
     for page in pages_with_code {
-        jit_dirty_page(ctx, page);
+        jit_dirty_page_ctx(ctx, page);
     }
 }
 
-pub fn jit_page_has_code(page: Page) -> bool { jit_page_has_code_ctx(get_jit_state(), page) }
+pub fn jit_page_has_code(page: Page) -> bool { jit_page_has_code_ctx(&mut get_jit_state(), page) }
 
-pub fn jit_page_has_code_ctx(ctx: &mut JitState, page: Page) -> bool {
+fn jit_page_has_code_ctx(ctx: &mut JitState, page: Page) -> bool {
     ctx.pages.contains_key(&page) || ctx.entry_points.contains_key(&page)
 }
 

From 23411f337e2318873e2cfbb5840ad1f67768dee3 Mon Sep 17 00:00:00 2001
From: Fabian 
Date: Fri, 20 Dec 2024 10:35:55 -0700
Subject: [PATCH 78/90] use emulator.destroy() over emulator.stop() to properly
 remove v86 instances

emulator.stop sometimes works (when the GC can figure out that .start()
is unreachable and nothing can call into the instance). However, some
resources, such as any WebSocket connection, need to be closed manually.
---
 examples/nodejs.js                 | 2 +-
 examples/nodejs_state.js           | 2 +-
 src/browser/main.js                | 2 +-
 tests/api/clean-shutdown.js        | 4 ++--
 tests/api/floppy-insert-eject.js   | 2 +-
 tests/api/reboot.js                | 2 +-
 tests/api/reset.js                 | 2 +-
 tests/api/serial.js                | 2 +-
 tests/api/state.js                 | 2 +-
 tests/benchmark/arch-bytemark.js   | 2 +-
 tests/benchmark/arch-python.js     | 2 +-
 tests/benchmark/linux-boot.js      | 2 +-
 tests/devices/fetch_network.js     | 1 -
 tests/devices/virtio_console.js    | 2 +-
 tests/devices/wisp_network.js      | 1 -
 tests/full/run.js                  | 1 -
 tests/jit-paging/run.js            | 2 +-
 tests/manual/gc.html               | 1 -
 tests/qemu/run.js                  | 2 +-
 tools/docker/alpine/build-state.js | 2 +-
 20 files changed, 17 insertions(+), 21 deletions(-)

diff --git a/examples/nodejs.js b/examples/nodejs.js
index f08b54d473..c2ec8246d0 100755
--- a/examples/nodejs.js
+++ b/examples/nodejs.js
@@ -38,7 +38,7 @@ process.stdin.on("data", function(c)
     if(c === "\u0003")
     {
         // ctrl c
-        emulator.stop();
+        emulator.destroy();
         process.stdin.pause();
     }
     else
diff --git a/examples/nodejs_state.js b/examples/nodejs_state.js
index e829cb093b..229f2c1456 100755
--- a/examples/nodejs_state.js
+++ b/examples/nodejs_state.js
@@ -42,7 +42,7 @@ process.stdin.on("data", async function(c)
     if(c === "\u0003")
     {
         // ctrl c
-        emulator.stop();
+        emulator.destroy();
         process.stdin.pause();
     }
     else if(c === "\x1b\x4f\x51")
diff --git a/src/browser/main.js b/src/browser/main.js
index a4581cb286..c65f443bf8 100644
--- a/src/browser/main.js
+++ b/src/browser/main.js
@@ -1997,7 +1997,7 @@
 
         $("exit").onclick = function()
         {
-            emulator.stop();
+            emulator.destroy();
             location.href = location.pathname;
         };
 
diff --git a/tests/api/clean-shutdown.js b/tests/api/clean-shutdown.js
index 5e011f9cfb..f645d822c6 100755
--- a/tests/api/clean-shutdown.js
+++ b/tests/api/clean-shutdown.js
@@ -1,7 +1,7 @@
 #!/usr/bin/env node
 "use strict";
 
-// This test checks that calling emulator.stop() will remove all event
+// This test checks that calling emulator.destroy() will remove all event
 // listeners, so that the nodejs process cleanly and automatically exits.
 
 const TEST_RELEASE_BUILD = +process.env.TEST_RELEASE_BUILD;
@@ -28,6 +28,6 @@ const emulator = new V86(config);
 setTimeout(function()
     {
         console.error("Calling stop()");
-        emulator.stop();
+        emulator.destroy();
         console.error("Called stop()");
     }, 3000);
diff --git a/tests/api/floppy-insert-eject.js b/tests/api/floppy-insert-eject.js
index 2f5aa98891..f6536e6c4f 100755
--- a/tests/api/floppy-insert-eject.js
+++ b/tests/api/floppy-insert-eject.js
@@ -44,7 +44,7 @@ setTimeout(async () =>
     emulator.keyboard_send_text("dir A:\n");
     await emulator.wait_until_vga_screen_contains("FDOS         ");
     console.log("Got FDOS");
-    emulator.stop();
+    emulator.destroy();
     clearTimeout(timeout);
     //clearInterval(interval);
 }, 1000);
diff --git a/tests/api/reboot.js b/tests/api/reboot.js
index 1cf54eb872..03b454d86c 100755
--- a/tests/api/reboot.js
+++ b/tests/api/reboot.js
@@ -44,7 +44,7 @@ emulator.add_listener("serial0-output-byte", function(byte)
         if(serial_text.endsWith("Files send via emulator appear in /mnt/"))
         {
             console.log("Ok");
-            emulator.stop();
+            emulator.destroy();
             clearTimeout(timeout);
         }
     }
diff --git a/tests/api/reset.js b/tests/api/reset.js
index d4657dd8cf..630d2abc2b 100755
--- a/tests/api/reset.js
+++ b/tests/api/reset.js
@@ -36,7 +36,7 @@ emulator.add_listener("serial0-output-byte", function(byte)
         serial_text = "";
         if(did_restart) {
             console.log("Ok");
-            emulator.stop();
+            emulator.destroy();
         }
         else {
             console.log("Calling restart()");
diff --git a/tests/api/serial.js b/tests/api/serial.js
index 88ab4c3a79..d9e1e45e37 100755
--- a/tests/api/serial.js
+++ b/tests/api/serial.js
@@ -49,6 +49,6 @@ emulator.add_listener("serial0-output-byte", function(byte)
         assert("da1fb5b421123c58080a59832675632505b8c139a8d7ecd1c31591ca5c65cea6" === hash.digest("hex"));
         console.log("ok");
         clearTimeout(timeout);
-        emulator.stop();
+        emulator.destroy();
     }
 });
diff --git a/tests/api/state.js b/tests/api/state.js
index 91a6180fe7..152e213657 100755
--- a/tests/api/state.js
+++ b/tests/api/state.js
@@ -87,7 +87,7 @@ async function run_test(name, config, done)
     }
 
     console.log("Done: %s", name);
-    emulator.stop();
+    emulator.destroy();
 }
 
 (async function() {
diff --git a/tests/benchmark/arch-bytemark.js b/tests/benchmark/arch-bytemark.js
index 887d6c91c8..a9d13c9b0d 100755
--- a/tests/benchmark/arch-bytemark.js
+++ b/tests/benchmark/arch-bytemark.js
@@ -69,7 +69,7 @@ emulator.add_listener("serial0-output-byte", function(byte)
 
     if(line === "* Trademarks are property of their respective holder.")
     {
-        emulator.stop();
+        emulator.destroy();
 
         if(BENCH_COLLECT_STATS)
         {
diff --git a/tests/benchmark/arch-python.js b/tests/benchmark/arch-python.js
index c68e6120be..1d16a10464 100755
--- a/tests/benchmark/arch-python.js
+++ b/tests/benchmark/arch-python.js
@@ -54,7 +54,7 @@ emulator.add_listener("serial0-output-byte", function(byte)
 
         if(line.startsWith("sys"))
         {
-            emulator.stop();
+            emulator.destroy();
 
             if(BENCH_COLLECT_STATS)
             {
diff --git a/tests/benchmark/linux-boot.js b/tests/benchmark/linux-boot.js
index 07e6c1b602..81529d7676 100755
--- a/tests/benchmark/linux-boot.js
+++ b/tests/benchmark/linux-boot.js
@@ -71,7 +71,7 @@ emulator.add_listener("serial0-output-byte", function(byte)
         const end_time = Date.now();
         const elapsed = end_time - start_time;
         console.log("Done in %dms", elapsed);
-        emulator.stop();
+        emulator.destroy();
 
         if(BENCH_COLLECT_STATS)
         {
diff --git a/tests/devices/fetch_network.js b/tests/devices/fetch_network.js
index 244ec5e2a3..373007c624 100755
--- a/tests/devices/fetch_network.js
+++ b/tests/devices/fetch_network.js
@@ -265,7 +265,6 @@ emulator.add_listener("serial0-output-byte", function(byte)
 
         if(test_num >= tests.length)
         {
-            emulator.stop();
             emulator.destroy();
 
             console.log("Tests finished.");
diff --git a/tests/devices/virtio_console.js b/tests/devices/virtio_console.js
index 6d88a1a5ba..beec4da408 100755
--- a/tests/devices/virtio_console.js
+++ b/tests/devices/virtio_console.js
@@ -59,7 +59,7 @@ emulator.add_listener("serial0-output-byte", function(byte)
     if(line.endsWith("pong"))
     {
         console.log("\nTest passed");
-        emulator.stop();
+        emulator.destroy();
     }
 });
 
diff --git a/tests/devices/wisp_network.js b/tests/devices/wisp_network.js
index b20af10fda..f8bad868e8 100755
--- a/tests/devices/wisp_network.js
+++ b/tests/devices/wisp_network.js
@@ -162,7 +162,6 @@ emulator.add_listener("serial0-output-byte", function(byte)
 
         if(test_num >= tests.length)
         {
-            emulator.stop();
             emulator.destroy();
 
             console.log("Tests finished.");
diff --git a/tests/full/run.js b/tests/full/run.js
index 744cadc827..4685166eb9 100755
--- a/tests/full/run.js
+++ b/tests/full/run.js
@@ -1146,7 +1146,6 @@ function run_test(test, done)
                 clearInterval(screen_interval);
             }
 
-            emulator.stop();
             emulator.destroy();
 
             if(test.failure_allowed)
diff --git a/tests/jit-paging/run.js b/tests/jit-paging/run.js
index 735e09d5e5..4bdc903cbf 100755
--- a/tests/jit-paging/run.js
+++ b/tests/jit-paging/run.js
@@ -63,7 +63,7 @@ emulator.add_listener("serial0-output-byte", async function(byte)
 
         const data = await emulator.read_file("/result");
 
-        emulator.stop();
+        emulator.destroy();
 
         let result = Buffer.from(data).toString();
         if(result !== "test_shared passed\ntest_consecutive_written passed\n")
diff --git a/tests/manual/gc.html b/tests/manual/gc.html
index 2c9651aedf..68053de9dd 100644
--- a/tests/manual/gc.html
+++ b/tests/manual/gc.html
@@ -19,7 +19,6 @@
     });
 
     setTimeout(() => {
-        emulator.stop();
         emulator.destroy();
         console.log("Emulator freed. Check using devtools (in chromium: Memory -> Heap Snapshot -> click collect garbage -> take snapshot).");
     }, 3 * 1000);
diff --git a/tests/qemu/run.js b/tests/qemu/run.js
index 51c593fc03..b4b0d7d2a9 100755
--- a/tests/qemu/run.js
+++ b/tests/qemu/run.js
@@ -64,6 +64,6 @@ emulator.add_listener("serial0-output-byte", async function(byte)
         console.error("Got result, writing to stdout");
 
         process.stdout.write(Buffer.from(data));
-        emulator.stop();
+        emulator.destroy();
     }
 });
diff --git a/tools/docker/alpine/build-state.js b/tools/docker/alpine/build-state.js
index 2a2f3341f8..fd866b9bfd 100755
--- a/tools/docker/alpine/build-state.js
+++ b/tools/docker/alpine/build-state.js
@@ -51,7 +51,7 @@ emulator.add_listener("serial0-output-byte", function(byte)
                     {
                         if(e) throw e;
                         console.log("Saved as " + OUTPUT_FILE);
-                        emulator.stop();
+                        emulator.destroy();
                     });
             }, 10 * 1000);
     }

From 21310a0c147383ed63e4247a6a303cf79a4b7af8 Mon Sep 17 00:00:00 2001
From: Fabian 
Date: Fri, 20 Dec 2024 11:14:06 -0700
Subject: [PATCH 79/90] update expect tests

A rust update changed the iteration order of hashtables, which
unfortunately affects these. This seems to happen rarely, so accept this
for now.
---
 tests/expect/tests/call-ret.wast      | 408 +++++++++++++-------------
 tests/expect/tests/indirect-call.wast | 348 +++++++++++-----------
 tests/expect/tests/sti.wast           |   2 +-
 3 files changed, 379 insertions(+), 379 deletions(-)

diff --git a/tests/expect/tests/call-ret.wast b/tests/expect/tests/call-ret.wast
index c59daa3bc7..b26df4a9ac 100644
--- a/tests/expect/tests/call-ret.wast
+++ b/tests/expect/tests/call-ret.wast
@@ -20,10 +20,10 @@
   (type $t18 (func (param i32 i64 i32)))
   (type $t19 (func (param i32 i64 i32) (result i32)))
   (type $t20 (func (param i32 i64 i64 i32) (result i32)))
+  (import "e" "instr_F4" (func $e.instr_F4 (type $t0)))
   (import "e" "safe_write32_slow_jit" (func $e.safe_write32_slow_jit (type $t16)))
   (import "e" "safe_read32s_slow_jit" (func $e.safe_read32s_slow_jit (type $t7)))
   (import "e" "jit_find_cache_entry_in_page" (func $e.jit_find_cache_entry_in_page (type $t16)))
-  (import "e" "instr_F4" (func $e.instr_F4 (type $t0)))
   (import "e" "trigger_fault_end_jit" (func $e.trigger_fault_end_jit (type $t0)))
   (import "e" "m" (memory {normalised output}))
   (func $f (export "f") (type $t1) (param $p0 i32)
@@ -72,234 +72,234 @@
                 (i32.add
                   (get_local $l8)
                   (i32.const 1)))
-              (set_local $l9
-                (i32.sub
-                  (i32.or
-                    (i32.and
-                      (i32.load
-                        (i32.const 556))
-                      (i32.const -4096))
-                    (i32.const 5))
-                  (i32.load
-                    (i32.const 740))))
-              (set_local $l11
-                (i32.add
-                  (tee_local $l10
-                    (i32.sub
-                      (get_local $l4)
-                      (i32.const 4)))
-                  (i32.load
-                    (i32.const 744))))
-              (block $B6
-                (br_if $B6
-                  (i32.and
-                    (i32.eq
-                      (i32.and
-                        (tee_local $l12
-                          (i32.load offset={normalised output}
-                            (i32.shl
-                              (i32.shr_u
-                                (get_local $l11)
-                                (i32.const 12))
-                              (i32.const 2))))
-                        (i32.const 4075))
-                      (i32.const 1))
-                    (i32.le_s
-                      (i32.and
-                        (get_local $l11)
-                        (i32.const 4095))
-                      (i32.const 4092))))
-                (br_if $B1
-                  (i32.and
-                    (tee_local $l12
-                      (call $e.safe_write32_slow_jit
-                        (get_local $l11)
-                        (get_local $l9)
-                        (i32.const 0)))
-                    (i32.const 1))))
-              (i32.store align=1
-                (i32.xor
+              (i32.store
+                (i32.const 560)
+                (i32.or
                   (i32.and
-                    (get_local $l12)
+                    (i32.load
+                      (i32.const 556))
                     (i32.const -4096))
-                  (get_local $l11))
-                (get_local $l9))
-              (set_local $l4
-                (get_local $l10))
-              (set_local $l8
-                (i32.add
-                  (get_local $l8)
-                  (i32.const 2)))
+                  (i32.const 5)))
               (i32.store
-                (i32.const 120)
+                (i32.const 556)
                 (i32.or
                   (i32.and
                     (i32.load
-                      (i32.const 120))
-                    (i32.const -2))
-                  (if $I7 (result i32)
-                    (i32.and
-                      (tee_local $l9
-                        (i32.load
-                          (i32.const 100)))
-                      (i32.const 1))
-                    (then
-                      (set_local $l9
-                        (i32.shr_s
-                          (get_local $l9)
-                          (i32.const 31)))
-                      (i32.lt_u
-                        (i32.xor
-                          (i32.load
-                            (i32.const 112))
-                          (get_local $l9))
-                        (i32.xor
-                          (i32.load
-                            (i32.const 104))
-                          (get_local $l9))))
-                    (else
-                      (i32.and
-                        (i32.load
-                          (i32.const 120))
-                        (i32.const 1))))))
+                      (i32.const 556))
+                    (i32.const -4096))
+                  (i32.const 6)))
               (i32.store
-                (i32.const 104)
+                (i32.const 64)
                 (get_local $l0))
-              (set_local $l0
-                (i32.add
-                  (get_local $l0)
-                  (i32.const 1)))
               (i32.store
-                (i32.const 112)
-                (get_local $l0))
-              (i64.store
-                (i32.const 96)
-                (i64.const 9706626088991))
-              (i32.const 0)
-              (set_local $l9
-                (i32.add
-                  (get_local $l4)
-                  (i32.load
-                    (i32.const 744))))
-              (block $B8
-                (br_if $B8
-                  (i32.and
-                    (i32.eq
-                      (i32.and
-                        (tee_local $l10
-                          (i32.load offset={normalised output}
-                            (i32.shl
-                              (i32.shr_u
-                                (get_local $l9)
-                                (i32.const 12))
-                              (i32.const 2))))
-                        (i32.const 4041))
-                      (i32.const 1))
-                    (i32.le_s
-                      (i32.and
-                        (get_local $l9)
-                        (i32.const 4095))
-                      (i32.const 4092))))
-                (br_if $B1
-                  (i32.and
-                    (tee_local $l10
-                      (call $e.safe_read32s_slow_jit
-                        (get_local $l9)
-                        (i32.const 7)))
-                    (i32.const 1))))
-              (i32.load align=1
-                (i32.xor
-                  (i32.and
-                    (get_local $l10)
-                    (i32.const -4096))
-                  (get_local $l9)))
+                (i32.const 68)
+                (get_local $l1))
+              (i32.store
+                (i32.const 72)
+                (get_local $l2))
+              (i32.store
+                (i32.const 76)
+                (get_local $l3))
+              (i32.store
+                (i32.const 80)
+                (get_local $l4))
+              (i32.store
+                (i32.const 84)
+                (get_local $l5))
+              (i32.store
+                (i32.const 88)
+                (get_local $l6))
+              (i32.store
+                (i32.const 92)
+                (get_local $l7))
+              (call $e.instr_F4)
+              (set_local $l0
+                (i32.load
+                  (i32.const 64)))
+              (set_local $l1
+                (i32.load
+                  (i32.const 68)))
+              (set_local $l2
+                (i32.load
+                  (i32.const 72)))
+              (set_local $l3
+                (i32.load
+                  (i32.const 76)))
               (set_local $l4
-                (i32.add
-                  (get_local $l4)
-                  (i32.const 4)))
-              (i32.load
-                (i32.const 740))
-              (i32.add)
-              (i32.store offset=556)
-              (br_if $L2
-                (i32.ge_s
-                  (tee_local $p0
-                    (call $e.jit_find_cache_entry_in_page
-                      (i32.load
-                        (i32.const 556))
-                      (i32.const 899)
-                      (i32.const 3)))
-                  (i32.const 0)))
+                (i32.load
+                  (i32.const 80)))
+              (set_local $l5
+                (i32.load
+                  (i32.const 84)))
+              (set_local $l6
+                (i32.load
+                  (i32.const 88)))
+              (set_local $l7
+                (i32.load
+                  (i32.const 92)))
               (br $B0))
             (set_local $l8
               (i32.add
                 (get_local $l8)
                 (i32.const 1)))
-            (i32.store
-              (i32.const 560)
-              (i32.or
+            (set_local $l9
+              (i32.sub
+                (i32.or
+                  (i32.and
+                    (i32.load
+                      (i32.const 556))
+                    (i32.const -4096))
+                  (i32.const 5))
+                (i32.load
+                  (i32.const 740))))
+            (set_local $l11
+              (i32.add
+                (tee_local $l10
+                  (i32.sub
+                    (get_local $l4)
+                    (i32.const 4)))
+                (i32.load
+                  (i32.const 744))))
+            (block $B6
+              (br_if $B6
                 (i32.and
-                  (i32.load
-                    (i32.const 556))
+                  (i32.eq
+                    (i32.and
+                      (tee_local $l12
+                        (i32.load offset={normalised output}
+                          (i32.shl
+                            (i32.shr_u
+                              (get_local $l11)
+                              (i32.const 12))
+                            (i32.const 2))))
+                      (i32.const 4075))
+                    (i32.const 1))
+                  (i32.le_s
+                    (i32.and
+                      (get_local $l11)
+                      (i32.const 4095))
+                    (i32.const 4092))))
+              (br_if $B1
+                (i32.and
+                  (tee_local $l12
+                    (call $e.safe_write32_slow_jit
+                      (get_local $l11)
+                      (get_local $l9)
+                      (i32.const 0)))
+                  (i32.const 1))))
+            (i32.store align=1
+              (i32.xor
+                (i32.and
+                  (get_local $l12)
                   (i32.const -4096))
-                (i32.const 5)))
+                (get_local $l11))
+              (get_local $l9))
+            (set_local $l4
+              (get_local $l10))
+            (set_local $l8
+              (i32.add
+                (get_local $l8)
+                (i32.const 2)))
             (i32.store
-              (i32.const 556)
+              (i32.const 120)
               (i32.or
                 (i32.and
                   (i32.load
-                    (i32.const 556))
-                  (i32.const -4096))
-                (i32.const 6)))
+                    (i32.const 120))
+                  (i32.const -2))
+                (if $I7 (result i32)
+                  (i32.and
+                    (tee_local $l9
+                      (i32.load
+                        (i32.const 100)))
+                    (i32.const 1))
+                  (then
+                    (set_local $l9
+                      (i32.shr_s
+                        (get_local $l9)
+                        (i32.const 31)))
+                    (i32.lt_u
+                      (i32.xor
+                        (i32.load
+                          (i32.const 112))
+                        (get_local $l9))
+                      (i32.xor
+                        (i32.load
+                          (i32.const 104))
+                        (get_local $l9))))
+                  (else
+                    (i32.and
+                      (i32.load
+                        (i32.const 120))
+                      (i32.const 1))))))
             (i32.store
-              (i32.const 64)
+              (i32.const 104)
               (get_local $l0))
-            (i32.store
-              (i32.const 68)
-              (get_local $l1))
-            (i32.store
-              (i32.const 72)
-              (get_local $l2))
-            (i32.store
-              (i32.const 76)
-              (get_local $l3))
-            (i32.store
-              (i32.const 80)
-              (get_local $l4))
-            (i32.store
-              (i32.const 84)
-              (get_local $l5))
-            (i32.store
-              (i32.const 88)
-              (get_local $l6))
-            (i32.store
-              (i32.const 92)
-              (get_local $l7))
-            (call $e.instr_F4)
             (set_local $l0
-              (i32.load
-                (i32.const 64)))
-            (set_local $l1
-              (i32.load
-                (i32.const 68)))
-            (set_local $l2
-              (i32.load
-                (i32.const 72)))
-            (set_local $l3
-              (i32.load
-                (i32.const 76)))
+              (i32.add
+                (get_local $l0)
+                (i32.const 1)))
+            (i32.store
+              (i32.const 112)
+              (get_local $l0))
+            (i64.store
+              (i32.const 96)
+              (i64.const 9706626088991))
+            (i32.const 0)
+            (set_local $l9
+              (i32.add
+                (get_local $l4)
+                (i32.load
+                  (i32.const 744))))
+            (block $B8
+              (br_if $B8
+                (i32.and
+                  (i32.eq
+                    (i32.and
+                      (tee_local $l10
+                        (i32.load offset={normalised output}
+                          (i32.shl
+                            (i32.shr_u
+                              (get_local $l9)
+                              (i32.const 12))
+                            (i32.const 2))))
+                      (i32.const 4041))
+                    (i32.const 1))
+                  (i32.le_s
+                    (i32.and
+                      (get_local $l9)
+                      (i32.const 4095))
+                    (i32.const 4092))))
+              (br_if $B1
+                (i32.and
+                  (tee_local $l10
+                    (call $e.safe_read32s_slow_jit
+                      (get_local $l9)
+                      (i32.const 7)))
+                  (i32.const 1))))
+            (i32.load align=1
+              (i32.xor
+                (i32.and
+                  (get_local $l10)
+                  (i32.const -4096))
+                (get_local $l9)))
             (set_local $l4
-              (i32.load
-                (i32.const 80)))
-            (set_local $l5
-              (i32.load
-                (i32.const 84)))
-            (set_local $l6
-              (i32.load
-                (i32.const 88)))
-            (set_local $l7
-              (i32.load
-                (i32.const 92)))
+              (i32.add
+                (get_local $l4)
+                (i32.const 4)))
+            (i32.load
+              (i32.const 740))
+            (i32.add)
+            (i32.store offset=556)
+            (br_if $L2
+              (i32.ge_s
+                (tee_local $p0
+                  (call $e.jit_find_cache_entry_in_page
+                    (i32.load
+                      (i32.const 556))
+                    (i32.const 899)
+                    (i32.const 3)))
+                (i32.const 0)))
             (br $B0))
           (unreachable)))
       (i32.store
diff --git a/tests/expect/tests/indirect-call.wast b/tests/expect/tests/indirect-call.wast
index 0f48efdc73..d0ebcee8e8 100644
--- a/tests/expect/tests/indirect-call.wast
+++ b/tests/expect/tests/indirect-call.wast
@@ -20,11 +20,11 @@
   (type $t18 (func (param i32 i64 i32)))
   (type $t19 (func (param i32 i64 i32) (result i32)))
   (type $t20 (func (param i32 i64 i64 i32) (result i32)))
-  (import "e" "instr_F4" (func $e.instr_F4 (type $t0)))
   (import "e" "trigger_gp_jit" (func $e.trigger_gp_jit (type $t2)))
   (import "e" "safe_read32s_slow_jit" (func $e.safe_read32s_slow_jit (type $t7)))
   (import "e" "safe_write32_slow_jit" (func $e.safe_write32_slow_jit (type $t16)))
   (import "e" "jit_find_cache_entry_in_page" (func $e.jit_find_cache_entry_in_page (type $t16)))
+  (import "e" "instr_F4" (func $e.instr_F4 (type $t0)))
   (import "e" "trigger_fault_end_jit" (func $e.trigger_fault_end_jit (type $t0)))
   (import "e" "m" (memory {normalised output}))
   (func $f (export "f") (type $t1) (param $p0 i32)
@@ -68,196 +68,196 @@
                 (br_if $B4
                   (i32.eq
                     (get_local $p0)
-                    (i32.const 1))))
+                    (i32.const 0))))
               (set_local $l8
                 (i32.add
                   (get_local $l8)
                   (i32.const 1)))
-              (i32.store
-                (i32.const 560)
-                (i32.or
+              (get_local $l0)
+              (if $I6
+                (i32.load8_u
+                  (i32.const 727))
+                (then
+                  (call $e.trigger_gp_jit
+                    (i32.const 0)
+                    (i32.const 0))
+                  (br $B1)))
+              (i32.load
+                (i32.const 748))
+              (i32.add)
+              (set_local $l9)
+              (block $B7
+                (br_if $B7
                   (i32.and
-                    (i32.load
-                      (i32.const 556))
-                    (i32.const -4096))
-                  (i32.const 2)))
-              (i32.store
-                (i32.const 556)
-                (i32.or
+                    (i32.eq
+                      (i32.and
+                        (tee_local $l10
+                          (i32.load offset={normalised output}
+                            (i32.shl
+                              (i32.shr_u
+                                (get_local $l9)
+                                (i32.const 12))
+                              (i32.const 2))))
+                        (i32.const 4041))
+                      (i32.const 1))
+                    (i32.le_s
+                      (i32.and
+                        (get_local $l9)
+                        (i32.const 4095))
+                      (i32.const 4092))))
+                (br_if $B1
                   (i32.and
-                    (i32.load
-                      (i32.const 556))
+                    (tee_local $l10
+                      (call $e.safe_read32s_slow_jit
+                        (get_local $l9)
+                        (i32.const 0)))
+                    (i32.const 1))))
+              (set_local $l9
+                (i32.add
+                  (i32.load align=1
+                    (i32.xor
+                      (i32.and
+                        (get_local $l10)
+                        (i32.const -4096))
+                      (get_local $l9)))
+                  (i32.load
+                    (i32.const 740))))
+              (set_local $l10
+                (i32.sub
+                  (i32.or
+                    (i32.and
+                      (i32.load
+                        (i32.const 556))
+                      (i32.const -4096))
+                    (i32.const 2))
+                  (i32.load
+                    (i32.const 740))))
+              (set_local $l12
+                (i32.add
+                  (tee_local $l11
+                    (i32.sub
+                      (get_local $l4)
+                      (i32.const 4)))
+                  (i32.load
+                    (i32.const 744))))
+              (block $B8
+                (br_if $B8
+                  (i32.and
+                    (i32.eq
+                      (i32.and
+                        (tee_local $l13
+                          (i32.load offset={normalised output}
+                            (i32.shl
+                              (i32.shr_u
+                                (get_local $l12)
+                                (i32.const 12))
+                              (i32.const 2))))
+                        (i32.const 4075))
+                      (i32.const 1))
+                    (i32.le_s
+                      (i32.and
+                        (get_local $l12)
+                        (i32.const 4095))
+                      (i32.const 4092))))
+                (br_if $B1
+                  (i32.and
+                    (tee_local $l13
+                      (call $e.safe_write32_slow_jit
+                        (get_local $l12)
+                        (get_local $l10)
+                        (i32.const 0)))
+                    (i32.const 1))))
+              (i32.store align=1
+                (i32.xor
+                  (i32.and
+                    (get_local $l13)
                     (i32.const -4096))
-                  (i32.const 3)))
-              (i32.store
-                (i32.const 64)
-                (get_local $l0))
-              (i32.store
-                (i32.const 68)
-                (get_local $l1))
-              (i32.store
-                (i32.const 72)
-                (get_local $l2))
-              (i32.store
-                (i32.const 76)
-                (get_local $l3))
-              (i32.store
-                (i32.const 80)
-                (get_local $l4))
-              (i32.store
-                (i32.const 84)
-                (get_local $l5))
-              (i32.store
-                (i32.const 88)
-                (get_local $l6))
-              (i32.store
-                (i32.const 92)
-                (get_local $l7))
-              (call $e.instr_F4)
-              (set_local $l0
-                (i32.load
-                  (i32.const 64)))
-              (set_local $l1
-                (i32.load
-                  (i32.const 68)))
-              (set_local $l2
-                (i32.load
-                  (i32.const 72)))
-              (set_local $l3
-                (i32.load
-                  (i32.const 76)))
+                  (get_local $l12))
+                (get_local $l10))
               (set_local $l4
-                (i32.load
-                  (i32.const 80)))
-              (set_local $l5
-                (i32.load
-                  (i32.const 84)))
-              (set_local $l6
-                (i32.load
-                  (i32.const 88)))
-              (set_local $l7
-                (i32.load
-                  (i32.const 92)))
+                (get_local $l11))
+              (i32.store offset=556
+                (i32.const 0)
+                (get_local $l9))
+              (br_if $L2
+                (i32.ge_s
+                  (tee_local $p0
+                    (call $e.jit_find_cache_entry_in_page
+                      (i32.load
+                        (i32.const 556))
+                      (i32.const 899)
+                      (i32.const 3)))
+                  (i32.const 0)))
               (br $B0))
             (set_local $l8
               (i32.add
                 (get_local $l8)
                 (i32.const 1)))
-            (get_local $l0)
-            (if $I6
-              (i32.load8_u
-                (i32.const 727))
-              (then
-                (call $e.trigger_gp_jit
-                  (i32.const 0)
-                  (i32.const 0))
-                (br $B1)))
-            (i32.load
-              (i32.const 748))
-            (i32.add)
-            (set_local $l9)
-            (block $B7
-              (br_if $B7
+            (i32.store
+              (i32.const 560)
+              (i32.or
                 (i32.and
-                  (i32.eq
-                    (i32.and
-                      (tee_local $l10
-                        (i32.load offset={normalised output}
-                          (i32.shl
-                            (i32.shr_u
-                              (get_local $l9)
-                              (i32.const 12))
-                            (i32.const 2))))
-                      (i32.const 4041))
-                    (i32.const 1))
-                  (i32.le_s
-                    (i32.and
-                      (get_local $l9)
-                      (i32.const 4095))
-                    (i32.const 4092))))
-              (br_if $B1
-                (i32.and
-                  (tee_local $l10
-                    (call $e.safe_read32s_slow_jit
-                      (get_local $l9)
-                      (i32.const 0)))
-                  (i32.const 1))))
-            (set_local $l9
-              (i32.add
-                (i32.load align=1
-                  (i32.xor
-                    (i32.and
-                      (get_local $l10)
-                      (i32.const -4096))
-                    (get_local $l9)))
-                (i32.load
-                  (i32.const 740))))
-            (set_local $l10
-              (i32.sub
-                (i32.or
-                  (i32.and
-                    (i32.load
-                      (i32.const 556))
-                    (i32.const -4096))
-                  (i32.const 2))
-                (i32.load
-                  (i32.const 740))))
-            (set_local $l12
-              (i32.add
-                (tee_local $l11
-                  (i32.sub
-                    (get_local $l4)
-                    (i32.const 4)))
-                (i32.load
-                  (i32.const 744))))
-            (block $B8
-              (br_if $B8
-                (i32.and
-                  (i32.eq
-                    (i32.and
-                      (tee_local $l13
-                        (i32.load offset={normalised output}
-                          (i32.shl
-                            (i32.shr_u
-                              (get_local $l12)
-                              (i32.const 12))
-                            (i32.const 2))))
-                      (i32.const 4075))
-                    (i32.const 1))
-                  (i32.le_s
-                    (i32.and
-                      (get_local $l12)
-                      (i32.const 4095))
-                    (i32.const 4092))))
-              (br_if $B1
-                (i32.and
-                  (tee_local $l13
-                    (call $e.safe_write32_slow_jit
-                      (get_local $l12)
-                      (get_local $l10)
-                      (i32.const 0)))
-                  (i32.const 1))))
-            (i32.store align=1
-              (i32.xor
+                  (i32.load
+                    (i32.const 556))
+                  (i32.const -4096))
+                (i32.const 2)))
+            (i32.store
+              (i32.const 556)
+              (i32.or
                 (i32.and
-                  (get_local $l13)
+                  (i32.load
+                    (i32.const 556))
                   (i32.const -4096))
-                (get_local $l12))
-              (get_local $l10))
+                (i32.const 3)))
+            (i32.store
+              (i32.const 64)
+              (get_local $l0))
+            (i32.store
+              (i32.const 68)
+              (get_local $l1))
+            (i32.store
+              (i32.const 72)
+              (get_local $l2))
+            (i32.store
+              (i32.const 76)
+              (get_local $l3))
+            (i32.store
+              (i32.const 80)
+              (get_local $l4))
+            (i32.store
+              (i32.const 84)
+              (get_local $l5))
+            (i32.store
+              (i32.const 88)
+              (get_local $l6))
+            (i32.store
+              (i32.const 92)
+              (get_local $l7))
+            (call $e.instr_F4)
+            (set_local $l0
+              (i32.load
+                (i32.const 64)))
+            (set_local $l1
+              (i32.load
+                (i32.const 68)))
+            (set_local $l2
+              (i32.load
+                (i32.const 72)))
+            (set_local $l3
+              (i32.load
+                (i32.const 76)))
             (set_local $l4
-              (get_local $l11))
-            (i32.store offset=556
-              (i32.const 0)
-              (get_local $l9))
-            (br_if $L2
-              (i32.ge_s
-                (tee_local $p0
-                  (call $e.jit_find_cache_entry_in_page
-                    (i32.load
-                      (i32.const 556))
-                    (i32.const 899)
-                    (i32.const 3)))
-                (i32.const 0)))
+              (i32.load
+                (i32.const 80)))
+            (set_local $l5
+              (i32.load
+                (i32.const 84)))
+            (set_local $l6
+              (i32.load
+                (i32.const 88)))
+            (set_local $l7
+              (i32.load
+                (i32.const 92)))
             (br $B0))
           (unreachable)))
       (i32.store
diff --git a/tests/expect/tests/sti.wast b/tests/expect/tests/sti.wast
index 47f284d150..59a4177721 100644
--- a/tests/expect/tests/sti.wast
+++ b/tests/expect/tests/sti.wast
@@ -67,7 +67,7 @@
                 (br_if $B4
                   (i32.eq
                     (get_local $p0)
-                    (i32.const 1))))
+                    (i32.const 0))))
               (set_local $l8
                 (i32.add
                   (get_local $l8)

From a347ff4b24db8d6ec5c8669b584fe8bab23fada3 Mon Sep 17 00:00:00 2001
From: SuperMaxusa <41739128+SuperMaxusa@users.noreply.github.com>
Date: Sun, 29 Dec 2024 15:47:07 +0200
Subject: [PATCH 80/90] alpine: update to 3.21

---
 tools/docker/alpine/Dockerfile | 23 ++++++-----------------
 1 file changed, 6 insertions(+), 17 deletions(-)

diff --git a/tools/docker/alpine/Dockerfile b/tools/docker/alpine/Dockerfile
index 0e7354b0c2..870cad2b8e 100644
--- a/tools/docker/alpine/Dockerfile
+++ b/tools/docker/alpine/Dockerfile
@@ -1,17 +1,9 @@
-FROM docker.io/i386/alpine:3.20.0
+FROM docker.io/i386/alpine:3.21.0
 
-ENV KERNEL=lts
+ENV KERNEL=virt
 ENV ADDPKGS=nodejs
 
-RUN apk add openrc alpine-base agetty alpine-conf $ADDPKGS
-
-RUN if [ "$KERNEL" == "lts" ]; then \
-    apk add linux-lts \
-            linux-firmware-none \
-            linux-firmware-sb16; \
-else \
-    apk add linux-$KERNEL; \
-fi
+RUN apk add openrc alpine-base agetty alpine-conf linux-$KERNEL linux-firmware-none $ADDPKGS
 
 RUN sed -i 's/getty 38400 tty1/agetty --autologin root tty1 linux/' /etc/inittab
 RUN echo 'ttyS0::respawn:/sbin/agetty --autologin root -s ttyS0 115200 vt100' >> /etc/inittab
@@ -19,11 +11,8 @@ RUN echo "root:" | chpasswd
 
 RUN setup-hostname localhost
 
-# Adding networking.sh script (works only on lts kernel yet)
-RUN if [ "$KERNEL" == "lts" ]; then \
-    echo -e "rmmod ne2k-pci && modprobe ne2k-pci\nhwclock -s\nsetup-interfaces -a -r" > /root/networking.sh && \
-    chmod +x /root/networking.sh; \
-fi
+# Adding networking.sh script
+RUN echo -e "rmmod ne2k-pci && modprobe ne2k-pci\nhwclock -s\nsetup-interfaces -a -r" > /root/networking.sh && chmod +x /root/networking.sh
 
 RUN echo 'console.log("Hello, world!");' > /root/hello.js
 
@@ -33,4 +22,4 @@ RUN for i in hwclock modules sysctl hostname syslog bootmisc; do rc-update add $
 RUN rc-update add killprocs shutdown
 
 # Generate initramfs with 9p modules
-RUN mkinitfs -F "ata base ide scsi virtio ext4 9p" $(cat /usr/share/kernel/$KERNEL/kernel.release)
+RUN mkinitfs -F "base virtio 9p" $(cat /usr/share/kernel/$KERNEL/kernel.release)

From ee8129b3cb0c2392442caa39947fd063a99da88e Mon Sep 17 00:00:00 2001
From: SuperMaxusa <41739128+SuperMaxusa@users.noreply.github.com>
Date: Sun, 29 Dec 2024 15:50:33 +0200
Subject: [PATCH 81/90] alpine: edit readme

(see https://gitlab.alpinelinux.org/alpine/aports/-/commit/b7d4635638106942beb648e0f59f3498eed81fef)
---
 tools/docker/alpine/Readme.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tools/docker/alpine/Readme.md b/tools/docker/alpine/Readme.md
index 1256f0481d..ac2dabbb1d 100644
--- a/tools/docker/alpine/Readme.md
+++ b/tools/docker/alpine/Readme.md
@@ -1,6 +1,6 @@
 You can build a Alpine Linux 9p image using Docker:
 
-1. As needed, kernel flavor (`virt` is smaller than `lts`, but don't have networking) and set of additional packages (community repo is enabled by default) can be edited in `Dockerfile`
+1. As needed, kernel flavor (`virt` is smaller than `lts`) and set of additional packages (community repo is enabled by default) can be edited in `Dockerfile`
 2. Check and run `./build.sh` with started dockerd (podman works)
 3. Run local webserver (e.g. `make run`) and open `examples/alpine.html`
 4. (optional) Run `./build-state.js` and add `initial_state: { url: "../images/alpine-state.bin" }` to `alpine.html`

From 1f3a3cec69e77feeb7ad89181f5a898a5725b78b Mon Sep 17 00:00:00 2001
From: SuperMaxusa <41739128+SuperMaxusa@users.noreply.github.com>
Date: Sun, 29 Dec 2024 15:54:52 +0200
Subject: [PATCH 82/90] alpine: added virtio-net support

---
 tools/docker/alpine/Dockerfile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tools/docker/alpine/Dockerfile b/tools/docker/alpine/Dockerfile
index 870cad2b8e..62b4c6dfde 100644
--- a/tools/docker/alpine/Dockerfile
+++ b/tools/docker/alpine/Dockerfile
@@ -12,7 +12,7 @@ RUN echo "root:" | chpasswd
 RUN setup-hostname localhost
 
 # Adding networking.sh script
-RUN echo -e "rmmod ne2k-pci && modprobe ne2k-pci\nhwclock -s\nsetup-interfaces -a -r" > /root/networking.sh && chmod +x /root/networking.sh
+RUN echo -e "rmmod ne2k-pci && modprobe ne2k-pci\nrmmod virtio-net && modprobe virtio-net\nhwclock -s\nsetup-interfaces -a -r" > /root/networking.sh && chmod +x /root/networking.sh
 
 RUN echo 'console.log("Hello, world!");' > /root/hello.js
 

From 7e30b3a93258865c31e6aa058347d22c544a81a6 Mon Sep 17 00:00:00 2001
From: SuperMaxusa <41739128+SuperMaxusa@users.noreply.github.com>
Date: Sun, 29 Dec 2024 18:30:30 +0200
Subject: [PATCH 83/90] ci: update actions from v3 to v4

---
 .github/workflows/ci.yml | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 20d02d3656..dd4ac97158 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -53,7 +53,7 @@ jobs:
         run: make rustfmt
 
       - name: Fetch kvm-unit-test cache
-        uses: actions/cache@v3
+        uses: actions/cache@v4
         id: cache-kvm-unit-test
         with:
           path: tests/kvm-unit-tests/
@@ -67,7 +67,7 @@ jobs:
         run: tests/kvm-unit-tests/run.js tests/kvm-unit-tests/x86/realmode.flat
 
       - name: Fetch namsmtests cache
-        uses: actions/cache@v3
+        uses: actions/cache@v4
         id: cache-nasmtests
         with:
           path: tests/nasm/build/
@@ -83,7 +83,7 @@ jobs:
         run: make rust-test
 
       - name: Fetch image cache
-        uses: actions/cache@v3
+        uses: actions/cache@v4
         id: cache-images
         with:
           path: images/
@@ -115,7 +115,7 @@ jobs:
         run: make expect-tests
 
       - name: Upload the artifact
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: v86
           path: |
@@ -138,7 +138,7 @@ jobs:
           github_token: ${{ secrets.GITHUB_TOKEN }}
 
       - name: Get artifacts
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: v86
           path: build

From 1e53213fec0b83ffdc2153a3a257b19baf51e7b8 Mon Sep 17 00:00:00 2001
From: Simone3838 
Date: Thu, 2 Jan 2025 20:07:21 +0100
Subject: [PATCH 84/90] add pillman

---
 src/browser/main.js | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/src/browser/main.js b/src/browser/main.js
index c65f443bf8..150e4fb98c 100644
--- a/src/browser/main.js
+++ b/src/browser/main.js
@@ -667,6 +667,15 @@
                 name: "bootLogo",
                 homepage: "https://github.com/nanochess/bootLogo",
             },
+            {
+                id: "pillman",
+                fda: {
+                    url: host + "pillman.img",
+                    size: 512,
+                },
+                name: "Pillman",
+                homepage: "https://github.com/nanochess/Pillman",
+            },
             {
                 id: "sectorlisp",
                 fda: {

From 6c999c5bf61716ea52f9a24a7d00d26313ae433c Mon Sep 17 00:00:00 2001
From: SuperMaxusa <41739128+SuperMaxusa@users.noreply.github.com>
Date: Sat, 4 Jan 2025 20:59:24 +0200
Subject: [PATCH 85/90] docs: Windows NT guest setup

(replaces "Windows XP/2000 guest setup")
---
 docs/windows-nt.md | 163 +++++++++++++++++++++++++++++++++++++++++++++
 docs/windows-xp.md |  73 --------------------
 2 files changed, 163 insertions(+), 73 deletions(-)
 create mode 100644 docs/windows-nt.md
 delete mode 100644 docs/windows-xp.md

diff --git a/docs/windows-nt.md b/docs/windows-nt.md
new file mode 100644
index 0000000000..fa010643aa
--- /dev/null
+++ b/docs/windows-nt.md
@@ -0,0 +1,163 @@
+## Windows NT 3.1
+
+### Installing using QEMU
+
+1. Install MS-DOS and [Oak CD-ROM Driver](https://www.dosdays.co.uk/topics/Software/optical_downloads.php).
+2. Create 4 blank floppy disk images:
+
+ - run `qemu-img create -f raw floppy.img 1440K`
+ - mount and run `format C:` in a VM
+
+3. Run QEMU with the following settings for installation:
+
+```sh
+qemu-system-i386 -m 64 -hda hdd.img -cpu pentium -M pc,acpi=off -cdrom InstallCD.iso
+```
+
+4. Run `xcopy /v :\I386\ C:\install\` in a VM to copy all files, disable the CD-ROM driver.
+5. Run QEMU with the following settings: 
+
+```sh
+qemu-system-i386 -m 64 -hda hdd.img -cpu pentium -M pc,acpi=off
+```
+
+6. Run `C:\install\winnt /F /C` in a VM.
+7. Follow the setup instructions. To change floppy disk, press *Ctrl+Alt+2* to switch to the QEMU Monitor, run `change floppy0 /path/to/new_floppy_image` and press *Ctrl+Alt+1* to switch to VGA.
+
+
+## Windows NT 3.51
+
+### Installing 
+
+> [!NOTE]
+> In newer versions of QEMU, the Windows Setup may not work, you can use an older version of QEMU, PCem, 86Box or PCBox instead.
+
+1. If you install via MS-DOS, install [the Oak CD-ROM Driver](https://www.dosdays.co.uk/topics/Software/optical_downloads.php) and run `:\I386\WINNT /B`.
+2. Follow the setup instructions.
+3. After installing, download NT 3.51 SuperPack ([here](https://bearwindows.zcm.com.au/winnt351.htm#4) or [here](https://alter.org.ua/en/soft/nt_spack/nt3/)), unpack the archive into a Windows and copy files from `FAT32` (`SYS\FAT32`) and `RENEW` (`SYS\RENEW`) folders in `C:\WINNT35\system32\drivers` with replacing.
+
+### Enabling networking
+
+1. Open "Control Panel" > "Network", install Windows NT Networking (installation CD required).
+2. In "Network Adapter Card Detection", press Continue three times, set `Network Adapter Card: Novell NE2000 Compatible Adapter`.
+3. Set the following settings and click Continue:
+
+```
+IRQ Level: 10
+I/O Port Address: 0x300
+```
+
+4. In "Bus Location", press OK. Check the boxes "TCP/IP Transport" and "Enable Automatic DHCP Configuration" in the next window.
+5. In "TCP/IP Configuration", check the box "Enable Automatic DHCP Configuration".
+6. Restart the VM.
+
+
+## Windows NT 4.0
+
+Recommended version: Windows NT 4.0 SP1
+
+### Installing using QEMU
+
+1. Run QEMU with the following settings for installation: 
+
+```sh
+qemu-system-i386 -m 64 -hda hdd.img -cdrom InstallCD.iso -cpu pentium -M pc,acpi=off
+```
+
+2. On setup startup, press F5 and select "Standard PC".
+3. Follow the setup instructions.
+
+### Running in v86
+
+Due to a problem with CPUID, you need to add `cpuid_level: 2` and `acpi: false` to the V86 constructor (not supported in the UI):
+
+```js
+var emulator = new V86({
+    ...
+    cpuid_level: 2,
+    acpi: false
+});
+```
+
+
+## Windows 2000/XP
+
+### Installing using QEMU
+
+1. Run QEMU with the following settings for installation: 
+
+```sh
+qemu-system-i386 -m 512 -hda hdd.img -cdrom InstallCD.iso
+```
+
+Optional:
+ - add `-device sb16` to enable sound
+ - add `-nic user,model=ne2k_pci` or `-device ne2k_pci,netdev=<...>` to enable networking
+
+2. Follow the setup instructions.
+3. This step fixes the error `Uncaught RangeError: Maximum call stack size exceeded` in Chromium during Windows 2000/XP startup in v86.
+
+After installation, change the computer type to "Standard PC" as described [here](http://web.archive.org/web/20220528021535/https://www.scm-pc-card.de/file/manual/FAQ/acpi_uninstallation_windows_xp_english.pdf):
+1. Open Start menu, right-click on "My Computer", select "Manage"
+2. Open Device Manager, open Computer, right-click on "ACPI Uniprocessor PC"
+3. Select "Update Driver..." > "No, not this time"
+4. Select "Install from a list or specific location (Advanced)" > Next > "Don't search. I will choose the driver to install." 
+5. Choose "Standard PC", press Next > Finish.
+6. Restart the VM, follow multiple "Found New Hardware Wizard" dialogs with default options.
+
+### Enabling True Color (for Windows 2000)
+
+> [!NOTE]
+> This driver doesn't support DirectX, DirectDraw and OpenGL.
+
+1. Download driver from https://bearwindows.zcm.com.au/vbemp.htm and unpack into Windows.
+2. Open Start menu, right-click on "My Computer", select "Manage"
+3. Open Device Manager, open Computer and right-click on "Video Controller".
+4. Press "Properties", select "Driver" tab and press "Update Driver".
+5. Select "Display a list of the known drivers for this device...", choose "Display adapters".
+5. Press "Have Disk...", click "Browse" and go to folder with unpacked driver. Go to `VBE20\W2K\PNP`, then select `vbemppnp.inf` inside.
+6. Select "VBE Miniport" adapter, press "Yes" and "Next".
+7. After installing, restart the VM.
+
+### Enabling sound
+
+*Source: [#1049](https://github.com/copy/v86/issues/1049)*
+
+1. Right-click on "My computer" > "System Properties", select "Hardware" tab, press "Hardware Wizard"
+2. Press "Next" > "Add/Troubleshoot a device" > "Add a new device"
+3. Select "No, I want to select the hardware from a list" > "Sound, video and game controllers"
+4. Select the following options and press "Next":
+
+```
+Hardware type: Sound, video and game cotrollers
+Manufacturers: Creative Technology Ltd.
+Models: Sound Blaster 16 or AWE32 or compatible (WDM)
+```
+
+
+## Windows Vista and newer
+
+### Installing using QEMU
+
+1. Run QEMU with the following settings for installation: 
+
+```sh
+qemu-system-i386 -m 1024 -hda hdd.img -cdrom InstallCD.iso
+```
+
+Optionally add `-accel kvm` (for Linux host), `-accel whpx` (for Windows host) or `-accel hvf` (for MacOS host) to use hypervisor acceleration.
+
+2. Follow the setup instructions.
+
+### Running in v86
+
+Enable ACPI and set the memory size to 512 MB or more.
+
+### Enabling networking (ne2k)
+
+Source: https://phaq.phunsites.net/2007/05/21/vista-on-xen-using-ne2000-in-favor-to-rtl8139/
+
+1. Download https://phaq.phunsites.net/files/2007/05/drivercd.iso_.zip, unpack the archive, mount the ISO to the VM (`-cdrom path/to/drivercd.iso` or `change ide1-cd0 path/to/drivercd.iso` in QEMU Monitor), unpack the archive from CDROM into Windows.
+2. Open Start Menu > "Control Panel" > "System" > "Device Manager"
+3. Right-click on "Ethernet Controller" > "Update Driver Software", press "Browse my computer for driver software".
+4. Click "Browse" and go to folder with unpacked driver, select `WIN2000` folder, press "Install this driver software anyway".
diff --git a/docs/windows-xp.md b/docs/windows-xp.md
deleted file mode 100644
index 2445cb43c7..0000000000
--- a/docs/windows-xp.md
+++ /dev/null
@@ -1,73 +0,0 @@
-*Most of this document also applies to Windows 2000.*
-
-You can download Windows 2000 from [WinWorld](https://winworldpc.com/download/413638c2-8d18-c39a-11c3-a4e284a2c3a5).
-Use QEMU to create `winxp.img`:
-
-```
-qemu-img create winxp.img 2G
-qemu-system-x86_64 -m 512 -drive file=winxp.img,format=raw -cdrom en_windows_xp_professional_with_service_pack_3_x86_cd_vl_x14-73974.iso
-```
-
-Follow the setup instructions.
-This step fixes the error `Uncaught RangeError: Maximum call stack size exceeded` in Chromium during Windows 2000/XP startup in v86.
-
-After installation, change the computer type to "Standard PC" as described [here](http://web.archive.org/web/20220528021535/https://www.scm-pc-card.de/file/manual/FAQ/acpi_uninstallation_windows_xp_english.pdf):
-Start > Right Click "My Computer" > Manage >
-Device Manager > Computer > Right Click "ACPI Uniprocessor PC" > Update Driver... >
-No, not this time > Next > Install from a list or specific location (Advanced) > Next >
-Don't search. I will choose the driver to install. > Next > Standard PC > Next > Finish.
-Restart the VM, follow multiple "Found New Hardware Wizard" dialogs with default options.
-
-Now, `winxp.img` is ready for v86. You can use [the website](https://copy.sh/v86/) to run it:
-Specify `winxp.img` as a hard disk, and optionally set the memory size to 512 MB.
-Or run it in a custom HTML file as described below.
-
-Get seabios.bin and vgabios.bin from [here](https://github.com/copy/v86/tree/master/bios),
-and get libv86.js and v86.wasm from [releases](https://github.com/copy/v86/releases/tag/latest).
-Create `winxp.htm` with this content (assuming all the files are in the same folder):
-
-```html
-
-
-
-
-
-
-
- -
-``` - -To open this HTML file locally, a HTTP server is needed. The standard Python server `python -m http.server` doesn't support HTTP range requests. -You can use [http-server](https://www.npmjs.com/package/http-server) or [devd](https://github.com/cortesi/devd). -Start the server (from the same folder as `winxp.htm`): -``` -npx http-server -``` -Open http://localhost:8080/winxp.htm in the browser. - -Windows XP load time (until start button becomes responsive) in Chromium on my computer: -* 3 min second time -* 4 min (first time or if cache is disabled) -* 12 min second time if Network tab in Developer Tools is open -* 17 min (first time or if cache is disabled) and Network tab in Developer Tools is open - -Sometimes Windows XP hangs after boot (before it is interactive) in v86, -displaying only desktop wallpaper without taskbar or desktop icons. From a2b1f3516e7653a0e2afe1dab3fa43b50d2f38f4 Mon Sep 17 00:00:00 2001 From: SuperMaxusa <41739128+SuperMaxusa@users.noreply.github.com> Date: Sat, 4 Jan 2025 21:02:57 +0200 Subject: [PATCH 86/90] docs: small fix in windows-9x.md --- docs/windows-9x.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/windows-9x.md b/docs/windows-9x.md index 1174070e1f..118e7961c2 100644 --- a/docs/windows-9x.md +++ b/docs/windows-9x.md @@ -8,7 +8,7 @@ Recommended versions: 1. Create a disk image (up to 2 GB): ```sh -qemu-img -f raw hdd.img M +qemu-img create -f raw hdd.img M ``` 2. Run QEMU with the following settings: ```sh From e0d0f8b958005ca60dd483471b8dc4a744d16848 Mon Sep 17 00:00:00 2001 From: SuperMaxusa <41739128+SuperMaxusa@users.noreply.github.com> Date: Sat, 4 Jan 2025 21:04:03 +0200 Subject: [PATCH 87/90] replace windows-xp.md to windows-nt.md --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index cbbc42f9d8..6c0858fa17 100644 --- a/Readme.md +++ b/Readme.md @@ -97,7 +97,7 @@ Here's an overview of the operating systems supported in v86: - There are some known boot issues ([#250](https://github.com/copy/v86/issues/250), [#433](https://github.com/copy/v86/issues/433), [#507](https://github.com/copy/v86/issues/507), [#555](https://github.com/copy/v86/issues/555), [#620](https://github.com/copy/v86/issues/620), [#645](https://github.com/copy/v86/issues/645)) - See [Windows 9x guest setup](docs/windows-9x.md) - Windows XP, Vista and 8 work under certain conditions (see [#86](https://github.com/copy/v86/issues/86), [#208](https://github.com/copy/v86/issues/208)) - - See [Windows 2000/XP guest setup](docs/windows-xp.md) + - See [Windows NT guest setup](docs/windows-nt.md) - Many hobby operating systems work. - 9front works. - Plan 9 doesn't work. From 11cd877bb84fedbde9486846eda277eba50e1fb5 Mon Sep 17 00:00:00 2001 From: SuperMaxusa <41739128+SuperMaxusa@users.noreply.github.com> Date: Sat, 4 Jan 2025 21:06:04 +0200 Subject: [PATCH 88/90] mention Windows NT setup guide in issue template --- .github/ISSUE_TEMPLATE/issue.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/issue.md b/.github/ISSUE_TEMPLATE/issue.md index 0659918436..8ee7d34d1f 100644 --- a/.github/ISSUE_TEMPLATE/issue.md +++ b/.github/ISSUE_TEMPLATE/issue.md @@ -12,7 +12,7 @@ Welcome to v86's issue tracker! We use this tracker for bug reports or feature requests. For user support, questions or general comments, use the chat at https://gitter.im/copy/v86 or the forum at https://github.com/copy/v86/discussions -Please don't ask for support for any version of Windows. There are many existing issues at https://github.com/copy/v86/issues?q=is%253Aissue+windows. See also docs/windows-xp.md and docs/windows-9x.md. +Please don't ask for support for any version of Windows. There are many existing issues at https://github.com/copy/v86/issues?q=is%253Aissue+windows. See also docs/windows-nt.md and docs/windows-9x.md. Before reporting OS incompatibilities, check existing issues and the compatibility section of the readme. From a7281d2c413e521692eee9a0986cae6329f092e6 Mon Sep 17 00:00:00 2001 From: SuperMaxusa <41739128+SuperMaxusa@users.noreply.github.com> Date: Sat, 4 Jan 2025 21:06:57 +0200 Subject: [PATCH 89/90] small fix in readme --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 6c0858fa17..fa47e7808a 100644 --- a/Readme.md +++ b/Readme.md @@ -62,7 +62,7 @@ list of emulated hardware: [Networking](docs/networking.md) — [Alpine Linux guest setup](tools/docker/alpine/) — [Arch Linux guest setup](docs/archlinux.md) — -[Windows 2000/XP guest setup](docs/windows-xp.md) — +[Windows NT guest setup](docs/windows-nt.md) — [Windows 9x guest setup](docs/windows-9x.md) — [9p filesystem](docs/filesystem.md) — [Linux rootfs on 9p](docs/linux-9p-image.md) — From 821a061e9d8e957f0fa161148884da1b3ff6030d Mon Sep 17 00:00:00 2001 From: SuperMaxusa <41739128+SuperMaxusa@users.noreply.github.com> Date: Mon, 6 Jan 2025 12:55:37 +0200 Subject: [PATCH 90/90] docs: added navigation by OS and fixed formatting for `windows-nt.md` --- docs/windows-nt.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/windows-nt.md b/docs/windows-nt.md index fa010643aa..e347488044 100644 --- a/docs/windows-nt.md +++ b/docs/windows-nt.md @@ -1,3 +1,9 @@ + + - [Windows NT 3.1](#windows-nt-31) / [3.51](#windows-nt-351) / [4.0](#windows-nt-40) + - [Windows 2000/XP](#windows-2000xp) + - [Windows Vista and newer](#windows-vista-and-newer) + +------------------------ ## Windows NT 3.1 ### Installing using QEMU @@ -6,7 +12,7 @@ 2. Create 4 blank floppy disk images: - run `qemu-img create -f raw floppy.img 1440K` - - mount and run `format C:` in a VM + - mount (`-fda floppy.img`) and run `format A:` in a VM 3. Run QEMU with the following settings for installation: @@ -155,7 +161,7 @@ Enable ACPI and set the memory size to 512 MB or more. ### Enabling networking (ne2k) -Source: https://phaq.phunsites.net/2007/05/21/vista-on-xen-using-ne2000-in-favor-to-rtl8139/ +*Source: https://phaq.phunsites.net/2007/05/21/vista-on-xen-using-ne2000-in-favor-to-rtl8139/* 1. Download https://phaq.phunsites.net/files/2007/05/drivercd.iso_.zip, unpack the archive, mount the ISO to the VM (`-cdrom path/to/drivercd.iso` or `change ide1-cd0 path/to/drivercd.iso` in QEMU Monitor), unpack the archive from CDROM into Windows. 2. Open Start Menu > "Control Panel" > "System" > "Device Manager"