diff --git a/crates/muvm/src/bin/muvm.rs b/crates/muvm/src/bin/muvm.rs index c785f42f..ad18ead3 100644 --- a/crates/muvm/src/bin/muvm.rs +++ b/crates/muvm/src/bin/muvm.rs @@ -16,7 +16,7 @@ use muvm::cli_options::options; use muvm::cpu::{get_fallback_cores, get_performance_cores}; use muvm::env::{find_muvm_exec, prepare_env_vars}; use muvm::hidpipe_server::spawn_hidpipe_server; -use muvm::launch::{launch_or_lock, LaunchResult}; +use muvm::launch::{launch_or_lock, LaunchResult, DYNAMIC_PORT_RANGE}; use muvm::monitor::spawn_monitor; use muvm::net::{connect_to_passt, start_passt}; use muvm::types::MiB; @@ -74,6 +74,7 @@ fn main() -> Result<()> { options.command, options.command_args, options.env, + options.interactive, )? { LaunchResult::LaunchRequested => { // There was a muvm instance already running and we've requested it @@ -297,6 +298,7 @@ fn main() -> Result<()> { return Err(err).context("Failed to configure vsock for pulse socket"); } } + 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( @@ -312,6 +314,25 @@ fn main() -> Result<()> { let err = Errno::from_raw_os_error(-err); return Err(err).context("Failed to configure vsock for hidpipe socket"); } + + let socket_dir = Path::new(&run_path).join("krun/socket"); + std::fs::create_dir_all(&socket_dir)?; + // Dynamic ports: Applications may listen on these sockets as neeeded. + for port in DYNAMIC_PORT_RANGE { + let socket_path = socket_dir.join(format!("port-{}", port)); + let socket_path = CString::new( + socket_path + .to_str() + .expect("socket_path should not contain invalid UTF-8"), + ) + .context("Failed to process dynamic socket path as it contains NUL character")?; + // SAFETY: `socket_path` is a pointer to a `CString` with long enough lifetime. + let err = unsafe { krun_add_vsock_port(ctx_id, port, socket_path.as_ptr()) }; + if err < 0 { + let err = Errno::from_raw_os_error(-err); + return Err(err).context("Failed to configure vsock for dynamic socket"); + } + } } // Forward the native X11 display into the guest as a socket diff --git a/crates/muvm/src/cli_options.rs b/crates/muvm/src/cli_options.rs index d5bd212f..8278c556 100644 --- a/crates/muvm/src/cli_options.rs +++ b/crates/muvm/src/cli_options.rs @@ -17,6 +17,7 @@ pub struct Options { pub server_port: u32, pub fex_images: Vec, pub direct_x11: bool, + pub interactive: bool, pub command: PathBuf, pub command_args: Vec, } @@ -116,7 +117,10 @@ pub fn options() -> OptionParser { "--direct-x11 requires the `x11bridge` crate feature", ) .hide(); - + let interactive = long("interactive") + .short('i') + .help("Allocate a tty guest-side and connect it to the current stdin/out") + .switch(); let command = positional("COMMAND").help("the command you want to execute in the vm"); let command_args = any::("COMMAND_ARGS", |arg| { (!["--help", "-h"].contains(&&*arg)).then_some(arg) @@ -134,6 +138,7 @@ pub fn options() -> OptionParser { server_port, fex_images, direct_x11, + interactive, // positionals command, command_args, diff --git a/crates/muvm/src/launch.rs b/crates/muvm/src/launch.rs index 69e4948b..2071276f 100644 --- a/crates/muvm/src/launch.rs +++ b/crates/muvm/src/launch.rs @@ -11,8 +11,14 @@ use anyhow::{anyhow, Context, Result}; use rustix::fs::{flock, FlockOperation}; use uuid::Uuid; +use super::utils::env::find_in_path; use crate::env::prepare_env_vars; use crate::utils::launch::Launch; +use rustix::path::Arg; +use std::ops::Range; +use std::process::{Child, Command}; + +pub const DYNAMIC_PORT_RANGE: Range = 50000..50200; pub enum LaunchResult { LaunchRequested, @@ -50,18 +56,79 @@ impl Display for LaunchError { } } +fn start_socat() -> Result<(Child, u32)> { + let run_path = env::var("XDG_RUNTIME_DIR") + .map_err(|e| anyhow!("unable to get XDG_RUNTIME_DIR: {:?}", e))?; + let socket_dir = Path::new(&run_path).join("krun/socket"); + let socat_path = + find_in_path("socat")?.ok_or_else(|| anyhow!("Unable to find socat in PATH"))?; + for port in DYNAMIC_PORT_RANGE { + let path = socket_dir.join(format!("port-{}", port)); + if path.exists() { + continue; + } + let child = Command::new(&socat_path) + .arg(format!("unix-l:{}", path.as_os_str().to_string_lossy())) + .arg("-,raw,echo=0") + .spawn()?; + return Ok((child, port)); + } + Err(anyhow!("Ran out of ports.")) +} + +fn escape_for_socat(s: String) -> String { + let mut ret = String::with_capacity(s.len()); + for c in s.chars() { + match c { + ':' | ',' | '!' | '"' | '\'' | '\\' | '(' | '[' | '{' => { + ret.push('\\'); + }, + _ => {}, + } + ret.push(c); + } + ret +} + +fn wrapped_launch( + server_port: u32, + cookie: Uuid, + mut command: PathBuf, + mut command_args: Vec, + env: HashMap, + interactive: bool, +) -> Result<()> { + if !interactive { + return request_launch(server_port, cookie, command, command_args, env); + } + let (mut socat, vsock_port) = start_socat()?; + command_args.insert(0, command.to_string_lossy().into_owned()); + command_args = vec![ + format!("vsock:2:{}", vsock_port), + format!( + "exec:{},pty,setsid,stderr", + escape_for_socat(command_args.join(" ")) + ), + ]; + command = "socat".into(); + request_launch(server_port, cookie, command, command_args, env)?; + socat.wait()?; + Ok(()) +} + pub fn launch_or_lock( server_port: u32, command: PathBuf, command_args: Vec, env: Vec<(String, Option)>, + interactive: bool, ) -> Result { let running_server_port = env::var("MUVM_SERVER_PORT").ok(); if let Some(port) = running_server_port { let port: u32 = port.parse()?; let env = prepare_env_vars(env)?; let cookie = read_cookie()?; - if let Err(err) = request_launch(port, cookie, command, command_args, env) { + if let Err(err) = wrapped_launch(port, cookie, command, command_args, env, interactive) { return Err(anyhow!("could not request launch to server: {err}")); } return Ok(LaunchResult::LaunchRequested); @@ -80,12 +147,13 @@ pub fn launch_or_lock( let env = prepare_env_vars(env)?; let mut tries = 0; loop { - match request_launch( + match wrapped_launch( server_port, cookie, command.clone(), command_args.clone(), env.clone(), + interactive, ) { Err(err) => match err.downcast_ref::() { Some(&LaunchError::Connection(_)) => {