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..5b0e2a09f0a --- /dev/null +++ b/build/vdagent.rs @@ -0,0 +1,83 @@ +//! 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 ` -machine none