update implementation based on feedback and added a pre-made websocket proxy

This commit is contained in:
Jeff Lindsay 2025-08-22 14:17:29 -07:00 committed by Fabian
parent f020b016f5
commit 1acdbbe785
4 changed files with 326 additions and 71 deletions

367
lib/9p.js
View file

@ -87,39 +87,17 @@ function range(size)
return Array.from(Array(size).keys());
}
/** @typedef {function(!Uint8Array, function(!Uint8Array):void):void} */
export let P9RequestHandler;
/**
* @constructor
*
* @param {(!FS|!P9RequestHandler)} fs_or_handler
* @param {CPU} cpu
* @param {Function} receive
*/
export function Virtio9p(fs_or_handler, cpu, bus) {
if (typeof fs_or_handler === "function") {
/** @type {(Function|undefined)} */
this.handle_fn = fs_or_handler;
this.tag_bufchain = new Map();
} else {
/** @type {(FS|undefined)} */
this.fs = fs_or_handler;
}
/** @const @type {BusConnector} */
this.bus = bus;
function Virtio9pDevice(cpu, receive) {
//this.configspace = [0x0, 0x4, 0x68, 0x6F, 0x73, 0x74]; // length of string and "host" string
//this.configspace = [0x0, 0x9, 0x2F, 0x64, 0x65, 0x76, 0x2F, 0x72, 0x6F, 0x6F, 0x74 ]; // length of string and "/dev/root" string
this.configspace_tagname = [0x68, 0x6F, 0x73, 0x74, 0x39, 0x70]; // "host9p" string
this.configspace_taglen = this.configspace_tagname.length; // num bytes
this.VERSION = "9P2000.L";
this.BLOCKSIZE = 8192; // Let's define one page.
this.msize = 8192; // maximum message size
this.replybuffer = new Uint8Array(this.msize*2); // Twice the msize to stay on the safe site
this.replybuffersize = 0;
this.fids = [];
/** @type {VirtIO} */
this.virtio = new VirtIO(cpu,
@ -164,7 +142,7 @@ export function Virtio9p(fs_or_handler, cpu, bus) {
while(this.virtqueue.has_request())
{
const bufchain = this.virtqueue.pop_request();
this.ReceiveRequest(bufchain);
receive(bufchain);
}
this.virtqueue.notify_me_after(0);
// Don't flush replies here: async replies are not completed yet.
@ -200,13 +178,37 @@ export function Virtio9p(fs_or_handler, cpu, bus) {
this.virtqueue = this.virtio.queues[0];
}
/**
* @constructor
*
* @param {FS} filesystem
* @param {CPU} cpu
*/
export function Virtio9p(filesystem, cpu, bus) {
/** @type {FS} */
this.fs = filesystem;
/** @const @type {BusConnector} */
this.bus = bus;
this.VERSION = "9P2000.L";
this.BLOCKSIZE = 8192; // Let's define one page.
this.msize = 8192; // maximum message size
this.replybuffer = new Uint8Array(this.msize*2); // Twice the msize to stay on the safe site
this.replybuffersize = 0;
this.fids = [];
this.device = new Virtio9pDevice(cpu, this.ReceiveRequest.bind(this));
}
Virtio9p.prototype.get_state = function()
{
var state = [];
state[0] = this.configspace_tagname;
state[1] = this.configspace_taglen;
state[2] = this.virtio;
state[0] = this.device.configspace_tagname;
state[1] = this.device.configspace_taglen;
state[2] = this.device.virtio;
state[3] = this.VERSION;
state[4] = this.BLOCKSIZE;
state[5] = this.msize;
@ -220,10 +222,10 @@ Virtio9p.prototype.get_state = function()
Virtio9p.prototype.set_state = function(state)
{
this.configspace_tagname = state[0];
this.configspace_taglen = state[1];
this.virtio.set_state(state[2]);
this.virtqueue = this.virtio.queues[0];
this.device.configspace_tagname = state[0];
this.device.configspace_taglen = state[1];
this.device.virtio.set_state(state[2]);
this.device.virtqueue = this.device.virtio.queues[0];
this.VERSION = state[3];
this.BLOCKSIZE = state[4];
this.msize = state[5];
@ -233,9 +235,7 @@ Virtio9p.prototype.set_state = function(state)
{
return { inodeid: f[0], type: f[1], uid: f[2], dbg_name: f[3] };
});
if (this.fs) {
this.fs.set_state(state[9]);
}
this.fs.set_state(state[9]);
};
// Note: dbg_name is only used for debugging messages and may not be the same as the filename,
@ -256,7 +256,7 @@ Virtio9p.prototype.update_dbg_name = function(idx, newname)
Virtio9p.prototype.reset = function() {
this.fids = [];
this.virtio.reset();
this.device.virtio.reset();
};
@ -280,8 +280,8 @@ Virtio9p.prototype.SendError = function (tag, errormsg, errorcode) {
Virtio9p.prototype.SendReply = function (bufchain) {
dbg_assert(this.replybuffersize >= 0, "9P: Negative replybuffersize");
bufchain.set_next_blob(this.replybuffer.subarray(0, this.replybuffersize));
this.virtqueue.push_reply(bufchain);
this.virtqueue.flush_replies();
this.device.virtqueue.push_reply(bufchain);
this.device.virtqueue.flush_replies();
};
Virtio9p.prototype.ReceiveRequest = async function (bufchain) {
@ -296,34 +296,6 @@ Virtio9p.prototype.ReceiveRequest = async function (bufchain) {
var tag = header[2];
//dbg_log("size:" + size + " id:" + id + " tag:" + tag, LOG_9P);
// if a 9p request handler was given
if (this.handle_fn) {
this.tag_bufchain.set(tag, bufchain);
this.handle_fn(buffer, (replybuf) => {
var reply_header = marshall.Unmarshall(["w", "b", "h"], replybuf, { offset: 0 });
var reply_tag = reply_header[2];
// Create a new buffer for each response instead of reusing the same one
this.replybuffer = new Uint8Array(replybuf.byteLength);
this.replybuffer.set(replybuf);
this.replybuffersize = replybuf.byteLength;
const bufchain = this.tag_bufchain.get(reply_tag);
if (!bufchain) {
console.error("No bufchain found for tag: " + reply_tag);
return;
}
bufchain.set_next_blob(replybuf);
this.virtqueue.push_reply(bufchain);
this.virtqueue.flush_replies();
this.tag_bufchain.delete(reply_tag);
});
return;
}
// otherwise, if a filesystem was given
switch(id)
{
case 8: // statfs
@ -920,3 +892,266 @@ Virtio9p.prototype.ReceiveRequest = async function (bufchain) {
//consistency checks if there are problems with the filesystem
//this.fs.Check();
};
/** @typedef {function(Uint8Array, function(Uint8Array):void):void} */
export let P9Handler;
/**
* @constructor
*
* @param {P9Handler} handle_fn
* @param {CPU} cpu
*/
export function Virtio9pHandler(handle_fn, cpu) {
/** @type {P9Handler} */
this.handle_fn = handle_fn;
this.tag_bufchain = new Map();
this.device = new Virtio9pDevice(cpu, async (bufchain) => {
// TODO: split into header + data blobs to avoid unnecessary copying.
const reqbuf = new Uint8Array(bufchain.length_readable);
bufchain.get_next_blob(reqbuf);
var reqheader = marshall.Unmarshall(["w", "b", "h"], reqbuf, { offset : 0 });
var reqtag = reqheader[2];
this.tag_bufchain.set(reqtag, bufchain);
this.handle_fn(reqbuf, (replybuf) => {
var replyheader = marshall.Unmarshall(["w", "b", "h"], replybuf, { offset: 0 });
var replytag = replyheader[2];
const bufchain = this.tag_bufchain.get(replytag);
if (!bufchain) {
console.error("No bufchain found for tag: " + replytag);
return;
}
bufchain.set_next_blob(replybuf);
this.device.virtqueue.push_reply(bufchain);
this.device.virtqueue.flush_replies();
this.tag_bufchain.delete(replytag);
});
});
}
Virtio9pHandler.prototype.get_state = function()
{
var state = [];
state[0] = this.device.configspace_tagname;
state[1] = this.device.configspace_taglen;
state[2] = this.device.virtio;
state[3] = this.tag_bufchain;
return state;
};
Virtio9pHandler.prototype.set_state = function(state)
{
this.device.configspace_tagname = state[0];
this.device.configspace_taglen = state[1];
this.device.virtio.set_state(state[2]);
this.device.virtqueue = this.device.virtio.queues[0];
this.tag_bufchain = state[3];
};
Virtio9pHandler.prototype.reset = function() {
this.device.virtio.reset();
};
/**
* @constructor
*
* @param {string} url
* @param {CPU} cpu
*/
export function Virtio9pProxy(url, cpu)
{
this.socket = undefined;
this.cpu = cpu;
// TODO: circular buffer?
this.send_queue = [];
this.url = url;
this.reconnect_interval = 10000;
this.last_connect_attempt = Date.now() - this.reconnect_interval;
this.send_queue_limit = 64;
this.destroyed = false;
this.tag_bufchain = new Map();
this.device = new Virtio9pDevice(cpu, async (bufchain) => {
// TODO: split into header + data blobs to avoid unnecessary copying.
const reqbuf = new Uint8Array(bufchain.length_readable);
bufchain.get_next_blob(reqbuf);
const reqheader = marshall.Unmarshall(["w", "b", "h"], reqbuf, { offset : 0 });
const reqtag = reqheader[2];
this.tag_bufchain.set(reqtag, bufchain);
this.send(reqbuf);
});
}
Virtio9pProxy.prototype.get_state = function()
{
var state = [];
state[0] = this.device.configspace_tagname;
state[1] = this.device.configspace_taglen;
state[2] = this.device.virtio;
state[3] = this.tag_bufchain;
return state;
};
Virtio9pProxy.prototype.set_state = function(state)
{
this.device.configspace_tagname = state[0];
this.device.configspace_taglen = state[1];
this.device.virtio.set_state(state[2]);
this.device.virtqueue = this.device.virtio.queues[0];
this.tag_bufchain = state[3];
};
Virtio9pProxy.prototype.reset = function() {
this.device.virtio.reset();
};
Virtio9pProxy.prototype.handle_message = function(e)
{
const replybuf = new Uint8Array(e.data);
const replyheader = marshall.Unmarshall(["w", "b", "h"], replybuf, { offset: 0 });
const replytag = replyheader[2];
const bufchain = this.tag_bufchain.get(replytag);
if (!bufchain) {
console.error("Virtio9pProxy: No bufchain found for tag: " + replytag);
return;
}
bufchain.set_next_blob(replybuf);
this.device.virtqueue.push_reply(bufchain);
this.device.virtqueue.flush_replies();
this.tag_bufchain.delete(replytag);
};
Virtio9pProxy.prototype.handle_close = function(e)
{
//console.log("onclose", e);
if(!this.destroyed)
{
this.connect();
setTimeout(this.connect.bind(this), this.reconnect_interval);
}
};
Virtio9pProxy.prototype.handle_open = function(e)
{
//console.log("open", e);
for(var i = 0; i < this.send_queue.length; i++)
{
this.send(this.send_queue[i]);
}
this.send_queue = [];
};
Virtio9pProxy.prototype.handle_error = function(e)
{
//console.log("onerror", e);
};
Virtio9pProxy.prototype.destroy = function()
{
this.destroyed = true;
if(this.socket)
{
this.socket.close();
}
};
Virtio9pProxy.prototype.connect = function()
{
if(typeof WebSocket === "undefined")
{
return;
}
if(this.socket)
{
var state = this.socket.readyState;
if(state === 0 || state === 1)
{
// already or almost there
return;
}
}
var now = Date.now();
if(this.last_connect_attempt + this.reconnect_interval > now)
{
return;
}
this.last_connect_attempt = Date.now();
try
{
this.socket = new WebSocket(this.url);
}
catch(e)
{
console.error(e);
return;
}
this.socket.binaryType = "arraybuffer";
this.socket.onopen = this.handle_open.bind(this);
this.socket.onmessage = this.handle_message.bind(this);
this.socket.onclose = this.handle_close.bind(this);
this.socket.onerror = this.handle_error.bind(this);
};
Virtio9pProxy.prototype.send = function(data)
{
//console.log("send", data);
if(!this.socket || this.socket.readyState !== 1)
{
this.send_queue.push(data);
if(this.send_queue.length > 2 * this.send_queue_limit)
{
this.send_queue = this.send_queue.slice(-this.send_queue_limit);
}
this.connect();
}
else
{
this.socket.send(data);
}
};
Virtio9pProxy.prototype.change_proxy = function(url)
{
this.url = url;
if(this.socket)
{
this.socket.onclose = function() {};
this.socket.onerror = function() {};
this.socket.close();
this.socket = undefined;
}
};

View file

@ -427,7 +427,12 @@ V86.prototype.continue_init = async function(emulator, options)
settings.handle9p = options.filesystem.handle9p;
}
if(options.filesystem && !options.filesystem.handle9p)
if(options.filesystem && options.filesystem.proxy_url)
{
settings.proxy9p = options.filesystem.proxy_url;
}
if(options.filesystem && !options.filesystem.handle9p && !options.filesystem.proxy_url)
{
var fs_url = options.filesystem.basefs;
var base_url = options.filesystem.baseurl;

View file

@ -33,7 +33,7 @@ import { IDEController } from "./ide.js";
import { VirtioNet } from "./virtio_net.js";
import { VGAScreen } from "./vga.js";
import { VirtioBalloon } from "./virtio_balloon.js";
import { Virtio9p } from "../lib/9p.js";
import { Virtio9p, Virtio9pHandler, Virtio9pProxy } from "../lib/9p.js";
import { load_kernel } from "./kernel.js";
@ -1162,9 +1162,17 @@ CPU.prototype.init = function(settings, device_bus)
this.devices.virtio_net = new VirtioNet(this, device_bus, settings.preserve_mac_from_state_image);
}
if(settings.fs9p || settings.handle9p)
if(settings.fs9p)
{
this.devices.virtio_9p = new Virtio9p(settings.fs9p || settings.handle9p, this, device_bus);
this.devices.virtio_9p = new Virtio9p(settings.fs9p, this, device_bus);
}
else if(settings.handle9p)
{
this.devices.virtio_9p = new Virtio9pHandler(settings.handle9p, this);
}
else if(settings.proxy9p)
{
this.devices.virtio_9p = new Virtio9pProxy(settings.proxy9p, this);
}
if(settings.virtio_console)
{

9
v86.d.ts vendored
View file

@ -238,9 +238,16 @@ export interface V86Options {
/**
* A function that will be called for each 9p request.
* If specified, this will back Virtio9p instead of a filesystem.
* Use this to connect Virtio9p to a custom 9p server.
* Use this to build or connect to a custom 9p server.
*/
handle9p?: (reqbuf: Uint8Array, reply: (replybuf: Uint8Array) => void) => void;
/**
* A URL to a websocket proxy for 9p.
* If specified, this will back Virtio9p instead of a filesystem.
* Use this to connect to a custom 9p server over websocket.
*/
proxy_url?: string;
};
/**