From 519b432156b3b68d1f602e6d61c9c63dd21cc982 Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 13 Oct 2026 05:47:41 +0200 Subject: [PATCH 1/6] A shared clipboard between the development host and ToyOS under QEMU, through QEMU's vdagent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every virtio machine gets QEMU's qemu-vdagent chardev on a virtio-serial function of its own, created just before the console's, with the agent's port `com.redhat.spice.0` at id 1 of 2 (build/vdagent.rs, read by cargo run's launch and by the harness). `cargo run` takes the display whose clipboard the host shares: cocoa on macOS, GTK with its clipboard on elsewhere. A QEMU without the chardev, or a non-macOS host without GTK, is refused by name before anything launches. The kernel: two 1af4:1043 functions now carry two devices. The console drives the first virtio console function that offers no MULTIPORT (toyos_pci::console), read by the first half of VirtioDevice::init, split out as VirtioDevice::offer, before any function masters the bus; a declined function is reset and left to a claim. A claim's vendor:device resolves among the functions the kernel does not drive (toyos_pci::claimed). /system/bin/vdagent claims the function, drives it over toyos-virtio and toyos-pci-claim, decodes the stream with toyos-vdagent (lib/vdagent, an input boundary of its own, after vd_agent.h at 499cc832 and virtio 1.2 §5.3), and makes every decision in toyos-vdagent-bridge, pure and host-tested. It pushes each guest copy with its text, answers no host REQUEST, and keeps QEMU 11.1.1's grab serials as measured black-box. The compositor serves a third port, `clipboard`, whose one connection is sent every client copy, one outstanding at a time, and whose sets land only while its ask is outstanding (toyos_desktop::clipboard); the build lets `vdagent` alone receive it. Two issues filed: every pcidev slot claimed on a cargo run boot, and the clipboard unmeasured on Linux and Windows hosts. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- Cargo.lock | 30 ++ Cargo.toml | 6 + build/build.rs | 11 +- build/lib.rs | 1 + build/qemu.rs | 29 +- build/vdagent.rs | 104 +++++ images/system.toml | 16 +- ...cargo-run-boot-claims-every-pcidev-slot.md | 24 + ...s-unmeasured-on-linux-and-windows-hosts.md | 20 + kernel/pci/src/choose.rs | 94 ++++ kernel/pci/src/lib.rs | 3 + kernel/src/drivers/virtio.rs | 99 +++-- kernel/src/drivers/virtio_console.rs | 41 +- kernel/src/pcidev/mod.rs | 56 +-- lib/manifest/src/clipboard.rs | 8 + lib/manifest/src/lib.rs | 4 +- lib/vdagent/Cargo.toml | 19 + lib/vdagent/src/control.rs | 143 ++++++ lib/vdagent/src/encode.rs | 216 +++++++++ lib/vdagent/src/lib.rs | 79 ++++ lib/vdagent/src/stream.rs | 420 ++++++++++++++++++ sdk/window/src/lib.rs | 6 + sdk/window/src/watcher.rs | 168 +++++++ system/compositor/desktop/src/budget.rs | 48 +- system/compositor/desktop/src/clipboard.rs | 163 +++++++ system/compositor/desktop/src/lib.rs | 8 +- system/compositor/src/client.rs | 24 +- system/compositor/src/main.rs | 11 +- system/compositor/src/session.rs | 277 ++++++++++-- system/vdagent/Cargo.toml | 19 + system/vdagent/bridge/Cargo.toml | 13 + system/vdagent/bridge/src/lib.rs | 305 +++++++++++++ system/vdagent/bridge/src/tests.rs | 295 ++++++++++++ system/vdagent/src/channel.rs | 321 +++++++++++++ system/vdagent/src/main.rs | 198 +++++++++ tests/common/iommu.rs | 5 +- tests/common/qemu.rs | 8 +- tests/images/clipboardcase.toml | 27 ++ tests/images/metalcase.toml | 2 +- tests/images/panelcase.toml | 2 +- tests/toyos.rs | 30 ++ 41 files changed, 3204 insertions(+), 149 deletions(-) create mode 100644 build/vdagent.rs create mode 100644 issues/a-cargo-run-boot-claims-every-pcidev-slot.md create mode 100644 issues/the-shared-clipboard-is-unmeasured-on-linux-and-windows-hosts.md create mode 100644 kernel/pci/src/choose.rs create mode 100644 lib/manifest/src/clipboard.rs create mode 100644 lib/vdagent/Cargo.toml create mode 100644 lib/vdagent/src/control.rs create mode 100644 lib/vdagent/src/encode.rs create mode 100644 lib/vdagent/src/lib.rs create mode 100644 lib/vdagent/src/stream.rs create mode 100644 sdk/window/src/watcher.rs create mode 100644 system/compositor/desktop/src/clipboard.rs create mode 100644 system/vdagent/Cargo.toml create mode 100644 system/vdagent/bridge/Cargo.toml create mode 100644 system/vdagent/bridge/src/lib.rs create mode 100644 system/vdagent/bridge/src/tests.rs create mode 100644 system/vdagent/src/channel.rs create mode 100644 system/vdagent/src/main.rs create mode 100644 tests/images/clipboardcase.toml diff --git a/Cargo.lock b/Cargo.lock index cc037e8b5dc..9f7a14a92a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4121,6 +4121,7 @@ dependencies = [ "toyos-tmpdir", "toyos-update", "toyos-userbound", + "toyos-vdagent", "toyos-wallclock", "toyos-xhci", "ureq", @@ -4495,6 +4496,20 @@ dependencies = [ "toyos-bootmap", ] +[[package]] +name = "toyos-vdagent" +version = "0.1.0" +dependencies = [ + "toyos-untrusted", +] + +[[package]] +name = "toyos-vdagent-bridge" +version = "0.1.0" +dependencies = [ + "toyos-vdagent", +] + [[package]] name = "toyos-virtio" version = "0.1.0" @@ -4735,6 +4750,21 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vdagent" +version = "0.0.0" +dependencies = [ + "toyos", + "toyos-abi", + "toyos-device-memory", + "toyos-manifest", + "toyos-pci-claim", + "toyos-vdagent", + "toyos-vdagent-bridge", + "toyos-virtio", + "toyos-window", +] + [[package]] name = "version_check" version = "0.9.5" diff --git a/Cargo.toml b/Cargo.toml index daaac56ffae..6a5699feb9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,7 @@ members = [ "lib/untrusted", "lib/update", "lib/userbound", + "lib/vdagent", "lib/virtio", "lib/wallclock", "lib/xhci", @@ -126,6 +127,8 @@ members = [ "system/update", "system/update/copies", "system/usbd", + "system/vdagent", + "system/vdagent/bridge", "tests/guest/kernelprobe", "tests/guest/metalprobe", "tests/guest/test-runner", @@ -216,6 +219,9 @@ toyos-osrelease = { path = "sdk/osrelease" } # The swap port's name and the one program the image may give it to, so the # gate on every manifest reads what the supervisor reads. toyos-swap = { path = "lib/swap" } +# QEMU's clipboard channel's port name, id and port count, so the launch reads +# the declaration the guest's agent drives. +toyos-vdagent = { path = "lib/vdagent" } # The watchdog's parameter name, so the gate over `kernel/src/params.rs` reads # the same constant the kernel and the bootloader do. toyos-tco = { path = "lib/tco" } diff --git a/build/build.rs b/build/build.rs index 0a0019a16c4..7dcec06119b 100644 --- a/build/build.rs +++ b/build/build.rs @@ -596,12 +596,14 @@ const SUPERVISOR_SERVED: &[&str] = &[toyos_swap::PORT, "power", toyos_manifest:: /// The ports that one row alone may receive, each with that row: the swap /// port, whose holder replaces any service's binary, the grants port, whose -/// holder writes which folder of the home a package sees, and the prompt -/// port, whose holder draws above every window and has every key while it does. +/// holder writes which folder of the home a package sees, the prompt port, +/// whose holder draws above every window and has every key while it does, and +/// the clipboard port, whose holder is sent every copy made in the session. const HELD_ALONE: &[(&str, &str)] = &[ (toyos_swap::PORT, toyos_swap::HOLDER), (toyos_manifest::grants::PORT, toyos_manifest::grants::HOLDER), (toyos_manifest::consent::PROMPT, toyos_manifest::consent::SERVER), + (toyos_manifest::clipboard::PORT, toyos_manifest::clipboard::HOLDER), ]; /// Who may hold the authorities that change what the machine runs or what a @@ -3012,6 +3014,7 @@ mod tests { "images/diag.toml", "images/console.toml", "tests/images/acpicase.toml", + "tests/images/clipboardcase.toml", "tests/images/hostedclangcase.toml", "tests/images/jobcase.toml", "tests/images/latencycase.toml", @@ -3098,6 +3101,10 @@ mod tests { assert!(held_by_their_holders_alone(&shell).is_err()); let apps: SystemConfig = toml::from_str("[apps]\nreceives = [\"grants\"]\n").unwrap(); assert!(held_by_their_holders_alone(&apps).is_err()); + let vdagent: SystemConfig = toml::from_str("[programs.vdagent]\nreceives = [\"clipboard\"]\n").unwrap(); + assert!(held_by_their_holders_alone(&vdagent).is_ok()); + let editor: SystemConfig = toml::from_str("[programs.editor]\nreceives = [\"clipboard\"]\n").unwrap(); + assert!(held_by_their_holders_alone(&editor).is_err()); } /// The prompt port reaches its server alone, and the consent port no row, diff --git a/build/lib.rs b/build/lib.rs index 421c86e0d19..c66d1d83e99 100644 --- a/build/lib.rs +++ b/build/lib.rs @@ -51,6 +51,7 @@ pub mod sysroot; pub mod tether; pub mod toolchain; pub mod userlandhost; +pub mod vdagent; pub mod wallpaper; use std::path::{Path, PathBuf}; diff --git a/build/qemu.rs b/build/qemu.rs index 002c7782844..8b0354010a8 100644 --- a/build/qemu.rs +++ b/build/qemu.rs @@ -83,7 +83,8 @@ pub enum Profile { struct Shape { /// The display device. `Virtio` is the only profile with no firmware GOP. virtio_gpu: bool, - /// virtio-net, virtio-sound, and the console on virtio-serial. + /// virtio-net, virtio-sound, the console on virtio-serial, and the + /// clipboard's channel on a virtio-serial function of its own. virtio: bool, /// A USB HID on the xHCI. The T14's keyboard is PS/2 and its touchpad /// I2C-HID; it has no USB HID at all. @@ -127,6 +128,17 @@ pub struct Options { pub fn launch(opts: &Options) { let shape = opts.profile.shape(); let arch = opts.arch; + // Before anything launches: every virtio machine carries the clipboard's + // channel, and every host's display is the one with its clipboard. + let display = display(); + let wanted = [ + shape.virtio.then(|| toyos_build::vdagent::available(arch)), + (display != "cocoa").then(|| toyos_build::vdagent::gtk(arch)), + ]; + if let Some(refused) = wanted.into_iter().flatten().find_map(Result::err) { + eprintln!("Error: {refused}"); + std::process::exit(1); + } let mut qemu = Command::new(arch.qemu()); // Without this QEMU runs its default-device pass whenever no network @@ -155,6 +167,7 @@ pub fn launch(opts: &Options) { if let Some(boot) = arch.boot() { qemu.arg("-boot").arg(boot); } + qemu.arg("-display").arg(display); qemu.arg("-machine") .arg(machine(arch, shape.iommu)) .arg("-smp") @@ -243,6 +256,10 @@ pub fn launch(opts: &Options) { qemu.arg("-device") .arg(format!("virtio-sound-pci,audiodev=audio0,streams=1{platform}")); + // The clipboard's channel, just before the console's function, so it + // enumerates first and the kernel's console rule runs. + qemu.args(toyos_build::vdagent::args(platform)); + // Console wiring: virtio-console on stdio is the primary I/O channel // (the kernel switches to it once virtio-console init completes — // see drivers/virtio_console.rs). UART stays on a file so early-boot @@ -340,6 +357,16 @@ fn iommu_device(arch: Arch) -> Option<&'static str> { } } +/// The display, with the host's clipboard: QEMU's own on macOS, and GTK's, +/// whose clipboard QEMU leaves off unless asked, everywhere else. +fn display() -> &'static str { + if cfg!(target_os = "macos") { + "cocoa" + } else { + "gtk,clipboard=on" + } +} + fn audio_backend() -> &'static str { if cfg!(target_os = "macos") { "coreaudio" diff --git a/build/vdagent.rs b/build/vdagent.rs new file mode 100644 index 00000000000..1f7db2eb20c --- /dev/null +++ b/build/vdagent.rs @@ -0,0 +1,104 @@ +//! QEMU's clipboard channel at the launch: its `qemu-vdagent` chardev, the +//! virtio-serial function of its own it is on, and the agent's port, from the +//! one declaration the guest's agent reads (`toyos-vdagent`). +//! +//! **A function of its own**, created just before the console's: the console's +//! function is the kernel's log channel from early boot, and the agent's +//! enumerates first, so every virtio boot runs the kernel's choice of console +//! (`toyos_pci::console`). +//! +//! **The chardev exists only in a QEMU built with spice-protocol** (QEMU's +//! `qapi/char.json`, `'if': 'CONFIG_SPICE_PROTOCOL'`), so a launch asks QEMU +//! for it first and refuses by name; nothing falls back. + +use std::process::Command; +use std::sync::OnceLock; + +use toyos_vdagent::{MAX_PORTS, PORT_ID, PORT_NAME}; + +use crate::arch::Arch; + +/// The arguments that give a guest the channel. `platform` is the virtio +/// function's `iommu_platform` property, as every other virtio function on the +/// machine is given it. +pub fn args(platform: &str) -> [String; 6] { + [ + "-chardev".into(), + "qemu-vdagent,id=vdagent0,clipboard=on,mouse=off".into(), + "-device".into(), + format!("virtio-serial-pci-non-transitional,id=vdagent-serial,max_ports={MAX_PORTS}{platform}"), + "-device".into(), + format!("virtserialport,bus=vdagent-serial.0,nr={PORT_ID},chardev=vdagent0,name={PORT_NAME}"), + ] +} + +/// Whether `arch`'s QEMU has the chardev, asked once per process. +pub fn available(arch: Arch) -> Result<(), String> { + static ASKED: [OnceLock>; 2] = [OnceLock::new(), OnceLock::new()]; + let slot = &ASKED[Arch::ALL.iter().position(|a| *a == arch).expect("every Arch is in ALL")]; + slot.get_or_init(|| { + let listed = help(arch, "-chardev")?; + named(&listed, "qemu-vdagent").then_some(()).ok_or_else(|| { + format!( + "{} has no `qemu-vdagent` chardev: it was built without spice-protocol, and every \ + virtio machine carries the shared clipboard's channel", + arch.qemu() + ) + }) + }) + .clone() +} + +/// Whether `arch`'s QEMU has the GTK display, which `cargo run` takes on a +/// host other than macOS for its clipboard. +pub fn gtk(arch: Arch) -> Result<(), String> { + let listed = help(arch, "-display")?; + named(&listed, "gtk").then_some(()).ok_or_else(|| { + format!("{} has no GTK display; Debian ships it as `qemu-system-gui`", arch.qemu()) + }) +} + +/// What `