This commit is contained in:
Fabian 2025-08-25 14:02:47 -06:00
parent 88aff1091f
commit 318003f117
3 changed files with 122 additions and 105 deletions

View file

@ -1,9 +1,9 @@
A 9p filesystem is supported by v86, using a virtio transport. There are several
ways it can be set up.
ways it can be set up.
### Guest mount
In all cases, the filesystem is mounted in the guest system using the `9p`
In all cases, the filesystem is mounted in the guest system using the `9p`
filesystem type and the `host9p` device tag. Typically you want to be specific
with the version and transport options:
@ -17,7 +17,7 @@ Here are kernel arguments you can use to boot directly off the 9p filesystem:
rw root=host9p rootfstype=9p rootflags=trans=virtio,version=9p2000.L
```
The `aname` option can be used to pick the directory from 9p to mount. The `rw`
The `aname` option can be used to pick the directory from 9p to mount. The `rw`
argument makes this a read-write root filesystem.
@ -25,7 +25,7 @@ argument makes this a read-write root filesystem.
This is the standard way to setup the 9p filesystem. It loads files over
HTTP on-demand into an in-memory filesystem in JS. This allows files to be
exchanged with the guest OS. See `create_file` and `read_file` in
exchanged with the guest OS. See `create_file` and `read_file` in
[`starter.js`](https://github.com/copy/v86/blob/master/src/browser/starter.js).
This mode is enabled by passing the following options to `V86`:
@ -41,7 +41,7 @@ Here, `basefs` is a json file created using
[fs2json](https://github.com/copy/fs2json). The base url is the prefix of a url
from which the files are available. For instance, if the 9p filesystem has a
file `/bin/sh`, that file must be accessible from
`http://localhost/9p/base/bin/sh`.
`http://localhost/9p/base/bin/sh`.
If `basefs` and `baseurl` are omitted, an empty 9p filesystem is created. Unless
you configure one of the alternative modes.
@ -84,16 +84,16 @@ Simlar to using `handle9p`, this filesystem will not be available in JS and
will need to be re-mounted after restoring state.
The WS proxy just needs to hand off messages with a connection to a normal 9p
server. Each binary WebSocket message is the full buffer of a request or a
reply.
server. Each binary WebSocket message is the full buffer of a request or a
reply.
To implement, request message bytes can just be sent directly to the 9p
To implement, request message bytes can just be sent directly to the 9p
connection, but the 9p reply stream needs to be buffered into a single binary
WebSocket message. The proxy must at least parse the first 4 bytes to get the
message size and use it to buffer a full message before sending over WebSocket.
WebSocket message. The proxy must at least parse the first 4 bytes to get the
message size and use it to buffer a full message before sending over WebSocket.
The [wanix](https://github.com/tractordev/wanix) CLI has a `serve` command that
not only serves a directory over HTTP, but also over 9P via WebSocket. You can
see how it [implements a proxy][1] in Go.
[1]: https://github.com/tractordev/wanix/blob/main/cmd/wanix/serve.go#L117-L177
[1]: https://github.com/tractordev/wanix/blob/main/cmd/wanix/serve.go#L117-L177

199
lib/9p.js
View file

@ -88,19 +88,12 @@ function range(size)
}
/**
* @constructor
*
* @param {CPU} cpu
* @param {Function} receive
*/
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
/** @type {VirtIO} */
this.virtio = new VirtIO(cpu,
function init_virtio(cpu, configspace_taglen, configspace_tagname, receive)
{
const virtio = new VirtIO(cpu,
{
name: "virtio-9p",
pci_id: 0x06 << 3,
@ -139,12 +132,13 @@ function Virtio9pDevice(cpu, receive) {
" (expected queue_id of 0)");
return;
}
while(this.virtqueue.has_request())
const virtqueue = virtio.queues[0];
while(virtqueue.has_request())
{
const bufchain = this.virtqueue.pop_request();
const bufchain = virtqueue.pop_request();
receive(bufchain);
}
this.virtqueue.notify_me_after(0);
virtqueue.notify_me_after(0);
// Don't flush replies here: async replies are not completed yet.
},
],
@ -161,7 +155,7 @@ function Virtio9pDevice(cpu, receive) {
{
bytes: 2,
name: "mount tag length",
read: () => this.configspace_taglen,
read: () => configspace_taglen,
write: data => { /* read only */ },
},
].concat(range(VIRTIO_9P_MAX_TAGLEN).map(index =>
@ -169,13 +163,13 @@ function Virtio9pDevice(cpu, receive) {
bytes: 1,
name: "mount tag name " + index,
// Note: configspace_tagname may have changed after set_state
read: () => this.configspace_tagname[index] || 0,
read: () => configspace_tagname[index] || 0,
write: data => { /* read only */ },
})
)),
},
});
this.virtqueue = this.virtio.queues[0];
return virtio;
}
/**
@ -191,24 +185,27 @@ export function Virtio9p(filesystem, cpu, bus) {
/** @const @type {BusConnector} */
this.bus = bus;
this.configspace_tagname = [0x68, 0x6F, 0x73, 0x74, 0x39, 0x70]; // "host9p" string
this.configspace_taglen = this.configspace_tagname.length; // num bytes
this.virtio = init_virtio(cpu, this.configspace_taglen, this.configspace_tagname, this.ReceiveRequest.bind(this));
this.virtqueue = this.virtio.queues[0];
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.device.configspace_tagname;
state[1] = this.device.configspace_taglen;
state[2] = this.device.virtio;
state[0] = this.configspace_tagname;
state[1] = this.configspace_taglen;
state[2] = this.virtio;
state[3] = this.VERSION;
state[4] = this.BLOCKSIZE;
state[5] = this.msize;
@ -222,10 +219,10 @@ Virtio9p.prototype.get_state = function()
Virtio9p.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.configspace_tagname = state[0];
this.configspace_taglen = state[1];
this.virtio.set_state(state[2]);
this.virtqueue = this.virtio.queues[0];
this.VERSION = state[3];
this.BLOCKSIZE = state[4];
this.msize = state[5];
@ -254,12 +251,12 @@ Virtio9p.prototype.update_dbg_name = function(idx, newname)
}
};
Virtio9p.prototype.reset = function() {
Virtio9p.prototype.reset = function()
{
this.fids = [];
this.device.virtio.reset();
this.virtio.reset();
};
Virtio9p.prototype.BuildReply = function(id, tag, payloadsize) {
dbg_assert(payloadsize >= 0, "9P: Negative payload size");
marshall.Marshall(["w", "b", "h"], [payloadsize+7, id+1, tag], this.replybuffer, 0);
@ -280,8 +277,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.device.virtqueue.push_reply(bufchain);
this.device.virtqueue.flush_replies();
this.virtqueue.push_reply(bufchain);
this.virtqueue.flush_replies();
};
Virtio9p.prototype.ReceiveRequest = async function (bufchain) {
@ -894,7 +891,7 @@ Virtio9p.prototype.ReceiveRequest = async function (bufchain) {
};
/** @typedef {function(Uint8Array, function(Uint8Array):void):void} */
export let P9Handler;
let P9Handler;
/**
* @constructor
@ -906,41 +903,52 @@ 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);
});
});
this.configspace_tagname = [0x68, 0x6F, 0x73, 0x74, 0x39, 0x70]; // "host9p" string
this.configspace_taglen = this.configspace_tagname.length; // num bytes
this.virtio = init_virtio(
cpu,
this.configspace_taglen,
this.configspace_tagname,
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.virtqueue.push_reply(bufchain);
this.virtqueue.flush_replies();
this.tag_bufchain.delete(replytag);
});
}
);
this.virtqueue = this.virtio.queues[0];
}
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[0] = this.configspace_tagname;
state[1] = this.configspace_taglen;
state[2] = this.virtio;
state[3] = this.tag_bufchain;
return state;
@ -948,16 +956,17 @@ Virtio9pHandler.prototype.get_state = function()
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.configspace_tagname = state[0];
this.configspace_taglen = state[1];
this.virtio.set_state(state[2]);
this.virtqueue = this.virtio.queues[0];
this.tag_bufchain = state[3];
};
Virtio9pHandler.prototype.reset = function() {
this.device.virtio.reset();
Virtio9pHandler.prototype.reset = function()
{
this.virtio.reset();
};
@ -982,26 +991,36 @@ export function Virtio9pProxy(url, cpu)
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);
});
this.configspace_tagname = [0x68, 0x6F, 0x73, 0x74, 0x39, 0x70]; // "host9p" string
this.configspace_taglen = this.configspace_tagname.length; // num bytes
this.virtio = init_virtio(
cpu,
this.configspace_taglen,
this.configspace_tagname,
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);
}
);
this.virtqueue = this.virtio.queues[0];
}
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[0] = this.configspace_tagname;
state[1] = this.configspace_taglen;
state[2] = this.virtio;
state[3] = this.tag_bufchain;
return state;
@ -1009,16 +1028,15 @@ Virtio9pProxy.prototype.get_state = function()
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.configspace_tagname = state[0];
this.configspace_taglen = state[1];
this.virtio.set_state(state[2]);
this.virtqueue = this.virtio.queues[0];
this.tag_bufchain = state[3];
};
Virtio9pProxy.prototype.reset = function() {
this.device.virtio.reset();
this.virtio.reset();
};
Virtio9pProxy.prototype.handle_message = function(e)
@ -1028,14 +1046,15 @@ Virtio9pProxy.prototype.handle_message = function(e)
const replytag = replyheader[2];
const bufchain = this.tag_bufchain.get(replytag);
if (!bufchain) {
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.virtqueue.push_reply(bufchain);
this.virtqueue.flush_replies();
this.tag_bufchain.delete(replytag);
};

View file

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