diff --git a/crates/muvm/Cargo.toml b/crates/muvm/Cargo.toml index 591c4324..4c33b7eb 100644 --- a/crates/muvm/Cargo.toml +++ b/crates/muvm/Cargo.toml @@ -26,7 +26,7 @@ serde = { version = "1.0.203", default-features = false, features = ["derive"] } serde_json = { version = "1.0.117", default-features = false, features = ["std"] } shell-words = { version = "1.1.1", default-features = false, features = ["std"] } tempfile = { version = "3.10.1", default-features = false, features = [] } -tokio = { version = "1.38.0", default-features = false, features = ["io-util", "macros", "net", "process", "rt-multi-thread", "sync"] } +tokio = { version = "1.38.0", default-features = false, features = ["io-util", "macros", "net", "process", "rt-multi-thread", "sync", "time"] } tokio-stream = { version = "0.1.15", default-features = false, features = ["net", "sync"] } udev = { version = "0.9.0", default-features = false, features = [] } uuid = { version = "1.10.0", default-features = false, features = ["serde", "std", "v7"] } diff --git a/crates/muvm/src/atspi.rs b/crates/muvm/src/atspi.rs new file mode 100644 index 00000000..e7d1a421 --- /dev/null +++ b/crates/muvm/src/atspi.rs @@ -0,0 +1,211 @@ +use std::collections::HashMap; +use std::env; +use std::ffi::OsString; +use std::fmt::Write as _; +use std::fs; +use std::io::ErrorKind; +use std::os::fd::AsRawFd; +use std::os::unix::ffi::{OsStrExt as _, OsStringExt as _}; +use std::os::unix::fs::symlink; +use std::path::{Component, Path, PathBuf}; +use std::process::Command; + +use anyhow::{bail, Context, Result}; +use log::debug; +use nix::sys::socket::{connect, socket, AddressFamily, SockFlag, SockType, UnixAddr}; + +pub struct HostBus { + path: PathBuf, + alias: bool, +} + +impl HostBus { + fn new(path: PathBuf, runtime_dir: &Path) -> Result { + if path.to_str().is_some() { + return Ok(Self { path, alias: false }); + } + + // libkrun requires UTF-8 even though Unix sockets accept arbitrary bytes. + // Its normal shutdown uses _exit, so reuse one runtime-directory alias + // under the launcher's VM lock instead of leaking a temporary directory. + let dir = runtime_dir.join("krun"); + let alias_path = dir.join("atspi"); + if alias_path.to_str().is_none() { + bail!("AT-SPI host alias path is not UTF-8"); + } + fs::create_dir_all(dir)?; + match fs::symlink_metadata(&alias_path) { + Ok(meta) if meta.file_type().is_symlink() => fs::remove_file(&alias_path)?, + Ok(_) => bail!("AT-SPI host alias {alias_path:?} already exists and is not a symlink"), + Err(err) if err.kind() == ErrorKind::NotFound => {}, + Err(err) => return Err(err.into()), + } + symlink(std::path::absolute(&path)?, &alias_path)?; + Ok(Self { + path: alias_path, + alias: true, + }) + } + + pub fn path(&self) -> &Path { + &self.path + } +} + +impl Drop for HostBus { + fn drop(&mut self) { + if self.alias { + // Clean up on startup errors; normal shutdown leaves the reusable + // symlink for the next launch or XDG_RUNTIME_DIR session cleanup. + let _ = fs::remove_file(&self.path); + } + } +} + +fn parse_unix_path_address(address: &str) -> Option { + if address.contains(';') { + return None; + } + let pairs = address.strip_prefix("unix:")?; + let mut path = None; + for pair in pairs.split(',') { + let (key, value) = pair.split_once('=')?; + if key == "path" { + path = Some(percent_decode(value)?); + } + } + path.filter(|p| !p.is_empty()).map(PathBuf::from) +} + +fn percent_decode(value: &str) -> Option { + let bytes = value.as_bytes(); + let mut out = Vec::with_capacity(bytes.len()); + let mut i = 0; + while i < bytes.len() { + if bytes[i] == b'%' { + let hex = bytes.get(i + 1..i + 3)?; + let hex = std::str::from_utf8(hex).ok()?; + out.push(u8::from_str_radix(hex, 16).ok()?); + i += 3; + } else { + out.push(bytes[i]); + i += 1; + } + } + Some(OsString::from_vec(out)) +} + +pub fn percent_encode(path: &Path) -> String { + let mut out = String::new(); + for &b in path.as_os_str().as_bytes() { + if b.is_ascii_alphanumeric() || b"-_/.\\*".contains(&b) { + out.push(b as char); + } else { + write!(out, "%{b:02X}").unwrap(); + } + } + out +} + +pub fn guest_socket_path(host_path: &Path, uid: u32) -> PathBuf { + // Preserve the path advertised by the host's AT_SPI_BUS X11 property. + // Non-UTF-8 paths need an alias because the guest configuration uses JSON. + let runtime_dir = PathBuf::from(format!("/run/user/{uid}")); + if host_path.starts_with(&runtime_dir) + && host_path.to_str().is_some() + && !host_path + .components() + .any(|part| part == Component::ParentDir) + { + host_path.to_path_buf() + } else { + runtime_dir.join("at-spi").join("bus_0") + } +} + +pub fn configure_guest_env(env: &mut HashMap, guest_path: &Path) { + env.entry("AT_SPI_BUS_ADDRESS".to_owned()) + .or_insert_with(|| format!("unix:path={}", percent_encode(guest_path))); + // The host cannot reach guest application sockets. + env.entry("ATSPI_DISABLE_P2P".to_owned()) + .or_insert_with(|| "1".to_owned()); + // Firefox otherwise waits for org.a11y.Status on the unbridged session bus. + env.entry("GNOME_ACCESSIBILITY".to_owned()) + .or_insert_with(|| "1".to_owned()); +} + +fn can_connect(path: &Path) -> bool { + // A full listen queue must not block startup. + let result = UnixAddr::new(path).and_then(|address| { + let fd = socket( + AddressFamily::Unix, + SockType::Stream, + SockFlag::SOCK_CLOEXEC | SockFlag::SOCK_NONBLOCK, + None, + )?; + connect(fd.as_raw_fd(), &address) + }); + if let Err(err) = result { + debug!(path:?, err:?; "AT-SPI bus socket is unavailable"); + } + result.is_ok() +} + +fn query_host_bus_address() -> Result { + let output = Command::new("busctl") + .args([ + "--user", + "--timeout=5", + "--json=short", + "call", + "org.a11y.Bus", + "/org/a11y/bus", + "org.a11y.Bus", + "GetAddress", + ]) + .output() + .context("failed to run busctl")?; + if !output.status.success() { + bail!( + "GetAddress failed: {}", + String::from_utf8_lossy(&output.stderr).trim() + ); + } + let reply: serde_json::Value = serde_json::from_slice(&output.stdout)?; + reply["data"][0] + .as_str() + .map(str::to_owned) + .context("GetAddress did not return an address") +} + +fn path_from_address(source: &str, address: &str) -> Option { + let path = parse_unix_path_address(address); + if path.is_none() { + eprintln!("AT-SPI bus address {address:?} from {source} is not a single unix:path address"); + } + path +} + +pub fn discover_host_bus(runtime_dir: &Path) -> Option { + let (source, address) = match env::var("AT_SPI_BUS_ADDRESS") + .ok() + .filter(|a| !a.is_empty()) + { + Some(address) => ("AT_SPI_BUS_ADDRESS", address), + None => match query_host_bus_address() { + Ok(address) => ("org.a11y.Bus.GetAddress", address), + Err(err) => { + debug!(err:?; "could not discover AT-SPI bus, accessibility bridge disabled"); + return None; + }, + }, + }; + let path = path_from_address(source, &address).filter(|path| can_connect(path))?; + HostBus::new(path, runtime_dir) + .inspect_err(|err| { + eprintln!( + "Failed to prepare host AT-SPI socket, accessibility bridge disabled: {err:#}" + ); + }) + .ok() +} diff --git a/crates/muvm/src/bin/muvm.rs b/crates/muvm/src/bin/muvm.rs index 5c21cfe7..d865abee 100644 --- a/crates/muvm/src/bin/muvm.rs +++ b/crates/muvm/src/bin/muvm.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::ffi::{c_char, CString}; use std::io::Write; use std::os::fd::{IntoRawFd, OwnedFd}; +use std::os::unix::ffi::OsStrExt as _; use std::path::Path; use std::process::ExitCode; use std::{env, fs}; @@ -15,6 +16,7 @@ use krun_sys::{ VIRGLRENDERER_USE_ASYNC_FENCE_CB, VIRGLRENDERER_USE_EGL, VIRGLRENDERER_VENUS, }; use log::debug; +use muvm::atspi::{configure_guest_env, discover_host_bus, guest_socket_path}; use muvm::cli_options::options; use muvm::config::Configuration; use muvm::cpu::{get_fallback_cores, get_performance_cores}; @@ -25,7 +27,7 @@ use muvm::monitor::spawn_monitor; use muvm::net::{connect_to_passt, start_passt}; use muvm::types::MiB; use muvm::utils::launch::{ - GuestConfiguration, Launch, HIDPIPE_SOCKET, MUVM_GUEST_SOCKET, PULSE_SOCKET, + GuestConfiguration, Launch, ATSPI_SOCKET, HIDPIPE_SOCKET, MUVM_GUEST_SOCKET, PULSE_SOCKET, }; use nix::sys::sysinfo::sysinfo; use nix::unistd::User; @@ -352,7 +354,7 @@ fn main() -> Result { } } - if let Some(run_path) = xdg_runtime_dir { + if let Some(run_path) = &xdg_runtime_dir { let hidpipe_path = Path::new(&run_path).join("hidpipe"); spawn_hidpipe_server(hidpipe_path.clone()).context("Failed to spawn hidpipe thread")?; let hidpipe_path = CString::new( @@ -406,6 +408,28 @@ fn main() -> Result { } let uid = getuid().as_raw(); + + // Retain any host socket alias until the VM exits. + let host_bus = xdg_runtime_dir + .as_deref() + .and_then(|path| discover_host_bus(Path::new(path))); + let atspi_socket = if let Some(host_bus) = &host_bus { + let host_path = host_bus.path(); + let host_path_c = CString::new(host_path.as_os_str().as_bytes()) + .context("Failed to process AT-SPI bus path as it contains NUL character")?; + // SAFETY: host_path_c is a pointer to a CString with long enough lifetime. + let err = unsafe { krun_add_vsock_port(ctx_id, ATSPI_SOCKET, host_path_c.as_ptr()) }; + if err < 0 { + let err = Errno::from_raw_os_error(-err); + return Err(err).context("Failed to configure vsock for AT-SPI socket"); + } + let guest_path = guest_socket_path(host_path, uid); + configure_guest_env(&mut env, &guest_path); + debug!(host:? = host_path, guest:? = guest_path; "AT-SPI bridge"); + Some(guest_path) + } else { + None + }; let user = User::from_uid(uid.into()) .map_err(Into::into) .and_then(|user| user.ok_or_else(|| anyhow!("requested entry not found"))) @@ -458,6 +482,7 @@ fn main() -> Result { cwd, init_commands, user_init_commands: options.user_init_commands, + atspi_socket, }; let mut muvm_config_file = NamedTempFile::new() .context("Failed to create a temporary file to store the muvm guest config")?; diff --git a/crates/muvm/src/guest/atspi.rs b/crates/muvm/src/guest/atspi.rs new file mode 100644 index 00000000..05b55dd9 --- /dev/null +++ b/crates/muvm/src/guest/atspi.rs @@ -0,0 +1,340 @@ +use std::collections::VecDeque; +use std::fmt::Write as _; +use std::fs; +use std::io::ErrorKind; +use std::os::fd::AsRawFd; +use std::os::unix::fs::DirBuilderExt as _; +use std::os::unix::net::{UnixListener as StdUnixListener, UnixStream as StdUnixStream}; +use std::path::Path; +use std::time::Duration; + +use anyhow::{anyhow, Context, Result}; +use log::debug; +use nix::libc::VMADDR_CID_HOST; +use nix::sys::socket::{connect, socket, AddressFamily, SockFlag, SockType, VsockAddr}; +use nix::unistd::getuid; +use tokio::io::{copy_bidirectional, AsyncReadExt as _, AsyncWriteExt as _}; +use tokio::net::{UnixListener, UnixStream}; + +use crate::utils::launch::ATSPI_SOCKET; + +#[derive(Default)] +struct SaslOutput { + to_server: Vec, + to_client: Vec, +} + +#[derive(PartialEq, Eq)] +enum ClientState { + Nul, + Lines, + Done, +} + +#[derive(PartialEq, Eq)] +enum Reply { + FromServer, + LocalError, +} + +// vsock cannot pass file descriptors. Reject NEGOTIATE_UNIX_FD locally, +// keeping replies ordered for clients that pipeline SASL commands. +// Each command gets one reply, except BEGIN. +struct SaslFilter { + external_identity: Vec, + authenticating_external: bool, + client_state: ClientState, + client_pending: Vec, + server_pending: Vec, + replies: VecDeque, +} + +impl SaslFilter { + const MAX_LINE: usize = 16 * 1024; + + fn new(uid: u32) -> Self { + let mut external_identity = String::new(); + for b in uid.to_string().bytes() { + write!(external_identity, "{b:02X}").unwrap(); + } + SaslFilter { + external_identity: external_identity.into_bytes(), + authenticating_external: false, + client_state: ClientState::Nul, + client_pending: Vec::new(), + server_pending: Vec::new(), + replies: VecDeque::new(), + } + } + + fn done(&self) -> bool { + self.client_state == ClientState::Done && self.replies.is_empty() + } + + fn drain_local_replies(&mut self, to_client: &mut Vec) { + while self.replies.front() == Some(&Reply::LocalError) { + self.replies.pop_front(); + to_client.extend_from_slice(b"ERROR\r\n"); + } + if self.done() { + // The last read may also contain D-Bus messages. + to_client.append(&mut self.server_pending); + } + } + + fn feed_client(&mut self, mut input: &[u8]) -> Result { + let mut out = SaslOutput::default(); + if self.client_state == ClientState::Nul { + let Some((&first, rest)) = input.split_first() else { + return Ok(out); + }; + if first != 0 { + return Err(anyhow!("D-Bus client did not start with a NUL byte")); + } + out.to_server.push(0); + self.client_state = ClientState::Lines; + input = rest; + } + if self.client_state == ClientState::Done { + out.to_server.extend_from_slice(input); + return Ok(out); + } + self.client_pending.extend_from_slice(input); + while let Some(end) = find_crlf(&self.client_pending) { + if end > Self::MAX_LINE { + return Err(anyhow!("D-Bus SASL line too long")); + } + let rest = self.client_pending.split_off(end + 2); + let line = std::mem::replace(&mut self.client_pending, rest); + if !line[..end] + .iter() + .all(|&b| b.is_ascii_graphic() || b == b' ' || b == b'\t') + { + return Err(anyhow!("Invalid byte in D-Bus SASL line")); + } + match sasl_command(&line[..end]) { + b"AUTH" => { + let response = external_auth_response(&line[..end]); + self.authenticating_external = response.is_some(); + if response.is_some_and(|r| !r.is_empty()) { + // The host sees the VM owner's credentials, including + // for privileged guest apps. + out.to_server.extend_from_slice(b"AUTH EXTERNAL "); + out.to_server.extend_from_slice(&self.external_identity); + out.to_server.extend_from_slice(b"\r\n"); + } else if response.is_some() { + // GDBus clients without an initial response expect a + // DATA challenge; preserve that exchange. + out.to_server.extend_from_slice(b"AUTH EXTERNAL\r\n"); + } else { + out.to_server.extend_from_slice(&line); + } + self.replies.push_back(Reply::FromServer); + }, + b"DATA" + if self.authenticating_external && sasl_response(&line[4..end]).is_some() => + { + out.to_server.extend_from_slice(b"DATA "); + out.to_server.extend_from_slice(&self.external_identity); + out.to_server.extend_from_slice(b"\r\n"); + self.replies.push_back(Reply::FromServer); + }, + b"NEGOTIATE_UNIX_FD" => self.replies.push_back(Reply::LocalError), + b"BEGIN" => { + // Invalid BEGIN leaves the server in SASL mode. + if !line[b"BEGIN".len()..end] + .iter() + .all(|&b| b == b' ' || b == b'\t') + { + return Err(anyhow!("D-Bus SASL BEGIN has arguments")); + } + // dbus-broker does not accept tab separators. + out.to_server.extend_from_slice(b"BEGIN\r\n"); + self.client_state = ClientState::Done; + out.to_server.append(&mut self.client_pending); + break; + }, + _ => { + if sasl_command(&line[..end]) == b"CANCEL" { + self.authenticating_external = false; + } + out.to_server.extend_from_slice(&line); + self.replies.push_back(Reply::FromServer); + }, + } + } + if self.client_pending.len() > Self::MAX_LINE { + return Err(anyhow!("D-Bus SASL line too long")); + } + self.drain_local_replies(&mut out.to_client); + Ok(out) + } + + fn feed_server(&mut self, input: &[u8]) -> Result> { + let mut to_client = Vec::new(); + if self.done() { + to_client.extend_from_slice(input); + return Ok(to_client); + } + self.server_pending.extend_from_slice(input); + while !self.done() { + let Some(end) = find_crlf(&self.server_pending) else { + break; + }; + if end > Self::MAX_LINE { + return Err(anyhow!("D-Bus SASL line too long")); + } + let rest = self.server_pending.split_off(end + 2); + let line = std::mem::replace(&mut self.server_pending, rest); + if sasl_command(&line[..end]) == b"AGREE_UNIX_FD" { + return Err(anyhow!("D-Bus server agreed to unix fd passing")); + } + to_client.extend_from_slice(&line); + match self.replies.pop_front() { + Some(Reply::FromServer) => {}, + Some(Reply::LocalError) => { + return Err(anyhow!("D-Bus SASL reply queue out of order")) + }, + None => return Err(anyhow!("D-Bus server sent an unexpected SASL line")), + } + self.drain_local_replies(&mut to_client); + } + if self.server_pending.len() > Self::MAX_LINE { + return Err(anyhow!("D-Bus SASL line too long")); + } + Ok(to_client) + } +} + +fn find_crlf(buf: &[u8]) -> Option { + buf.windows(2).position(|w| w == b"\r\n") +} + +fn sasl_command(line: &[u8]) -> &[u8] { + // dbus-daemon also accepts tab separators. + line.split(|&b| b == b' ' || b == b'\t') + .next() + .unwrap_or(line) +} + +fn external_auth_response(line: &[u8]) -> Option<&[u8]> { + let mut args = line + .split(|&b| b == b' ' || b == b'\t') + .filter(|arg| !arg.is_empty()); + if args.next() != Some(b"AUTH") || args.next() != Some(b"EXTERNAL") { + return None; + } + let response = args.next().unwrap_or_default(); + if args.next().is_some() { + return None; + } + sasl_response(response) +} + +fn sasl_response(response: &[u8]) -> Option<&[u8]> { + let response = response.trim_ascii(); + (response.len() % 2 == 0 && response.iter().all(u8::is_ascii_hexdigit)).then_some(response) +} + +pub fn bind_atspi_socket(path: &Path) -> Result { + let dir = path + .parent() + .ok_or_else(|| anyhow!("AT-SPI socket path {path:?} has no parent directory"))?; + fs::DirBuilder::new() + .recursive(true) + .mode(0o700) + .create(dir) + .with_context(|| format!("Failed to create {dir:?}"))?; + // /run is a guest tmpfs, so this cannot unlink the host socket. + match fs::remove_file(path) { + Ok(()) => {}, + Err(err) if err.kind() == ErrorKind::NotFound => {}, + Err(err) => return Err(err).with_context(|| format!("Failed to remove stale {path:?}")), + } + let listener = + StdUnixListener::bind(path).with_context(|| format!("Failed to bind {path:?}"))?; + listener + .set_nonblocking(true) + .context("Failed to set AT-SPI listener nonblocking")?; + Ok(listener) +} + +fn connect_host() -> Result { + let fd = socket( + AddressFamily::Vsock, + SockType::Stream, + SockFlag::SOCK_CLOEXEC, + None, + ) + .context("Failed to create vsock socket")?; + connect( + fd.as_raw_fd(), + &VsockAddr::new(VMADDR_CID_HOST, ATSPI_SOCKET), + ) + .context("Failed to connect to host AT-SPI bus over vsock")?; + let stream = StdUnixStream::from(fd); + stream + .set_nonblocking(true) + .context("Failed to set vsock stream nonblocking")?; + Ok(stream) +} + +async fn proxy_connection(mut client: UnixStream) -> Result<()> { + let server = tokio::task::spawn_blocking(connect_host) + .await + .context("vsock connect task failed")??; + let mut server = UnixStream::from_std(server)?; + + let mut filter = SaslFilter::new(getuid().as_raw()); + let mut client_buf = [0u8; 4096]; + let mut server_buf = [0u8; 4096]; + while !filter.done() { + tokio::select! { + n = client.read(&mut client_buf) => { + let n = n?; + if n == 0 { + return Ok(()); + } + let out = filter.feed_client(&client_buf[..n])?; + server.write_all(&out.to_server).await?; + client.write_all(&out.to_client).await?; + }, + n = server.read(&mut server_buf) => { + let n = n?; + if n == 0 { + return Ok(()); + } + let to_client = filter.feed_server(&server_buf[..n])?; + client.write_all(&to_client).await?; + }, + } + } + copy_bidirectional(&mut client, &mut server).await?; + Ok(()) +} + +pub async fn run_atspi_bridge(listener: StdUnixListener) { + let listener = match UnixListener::from_std(listener) { + Ok(listener) => listener, + Err(err) => { + eprintln!("AT-SPI bridge: failed to register listener: {err}"); + return; + }, + }; + loop { + match listener.accept().await { + Ok((client, _)) => { + tokio::spawn(async move { + if let Err(err) = proxy_connection(client).await { + debug!(err:? = err; "AT-SPI bridge connection closed"); + } + }); + }, + Err(err) => { + eprintln!("AT-SPI bridge: failed to accept incoming connection: {err}"); + // Back off on temporary errors such as EMFILE. + tokio::time::sleep(Duration::from_secs(1)).await; + }, + } + } +} diff --git a/crates/muvm/src/guest/bin/muvm-guest.rs b/crates/muvm/src/guest/bin/muvm-guest.rs index 6bac1838..eeba2243 100644 --- a/crates/muvm/src/guest/bin/muvm-guest.rs +++ b/crates/muvm/src/guest/bin/muvm-guest.rs @@ -6,6 +6,7 @@ use std::process::{Command, ExitCode}; use std::{cmp, env, fs, thread}; use anyhow::{anyhow, Context, Result}; +use muvm::guest::atspi::{bind_atspi_socket, run_atspi_bridge}; use muvm::guest::box64::setup_box; use muvm::guest::bridge::pipewire::start_pwbridge; use muvm::guest::bridge::x11::start_x11bridge; @@ -127,15 +128,13 @@ fn main() -> Result { Err(err) => return Err(err).context("Failed to set up user, bailing out"), }; - for init_command in options.user_init_commands { - let code = Command::new(&init_command) - .current_dir(&options.cwd) - .spawn()? - .wait()?; - if !code.success() { - return Err(anyhow!("Executing `{}` failed", init_command.display())); - } - } + let atspi_listener = options.atspi_socket.as_deref().and_then(|path| { + bind_atspi_socket(path) + .inspect_err(|err| { + eprintln!("Failed to set up AT-SPI bridge, accessibility will not work: {err:#}"); + }) + .ok() + }); let pulse_path = run_path.join("pulse"); std::fs::create_dir(&pulse_path) @@ -161,5 +160,20 @@ fn main() -> Result { }); let rt = tokio::runtime::Runtime::new().unwrap(); + if let Some(listener) = atspi_listener { + rt.spawn(run_atspi_bridge(listener)); + } + + // Init commands may connect to AT-SPI before the guest server starts. + for init_command in options.user_init_commands { + let code = Command::new(&init_command) + .current_dir(&options.cwd) + .spawn()? + .wait()?; + if !code.success() { + return Err(anyhow!("Executing `{}` failed", init_command.display())); + } + } + rt.block_on(async { server_main(options.command.command, options.command.command_args).await }) } diff --git a/crates/muvm/src/guest/mod.rs b/crates/muvm/src/guest/mod.rs index 44893571..5987c9d1 100644 --- a/crates/muvm/src/guest/mod.rs +++ b/crates/muvm/src/guest/mod.rs @@ -1,3 +1,4 @@ +pub mod atspi; pub mod box64; pub mod bridge; pub mod fex; diff --git a/crates/muvm/src/lib.rs b/crates/muvm/src/lib.rs index 50cc5175..acd432aa 100644 --- a/crates/muvm/src/lib.rs +++ b/crates/muvm/src/lib.rs @@ -1,3 +1,4 @@ +pub mod atspi; pub mod cli_options; pub mod config; pub mod cpu; diff --git a/crates/muvm/src/utils/launch.rs b/crates/muvm/src/utils/launch.rs index 6a50d5a6..562068b6 100644 --- a/crates/muvm/src/utils/launch.rs +++ b/crates/muvm/src/utils/launch.rs @@ -46,8 +46,10 @@ pub struct GuestConfiguration { pub cwd: PathBuf, pub init_commands: Vec, pub user_init_commands: Vec, + pub atspi_socket: Option, } pub const PULSE_SOCKET: u32 = 3333; pub const HIDPIPE_SOCKET: u32 = PULSE_SOCKET + 1; pub const MUVM_GUEST_SOCKET: u32 = HIDPIPE_SOCKET + 1; +pub const ATSPI_SOCKET: u32 = MUVM_GUEST_SOCKET + 1;