Skip to content

Commit

Permalink
muvm: Replace socat for interactive mode.
Browse files Browse the repository at this point in the history
Socat's tty mode has issues with window resizing
as it does not pass through SIGWINCH. Also add
a tty-less interactive mode, for usage from scripts
and similar.

Signed-off-by: Sasha Finkelstein <[email protected]>
  • Loading branch information
WhatAmISupposedToPutHere committed Dec 7, 2024
1 parent 59b4934 commit 38720e2
Show file tree
Hide file tree
Showing 13 changed files with 467 additions and 73 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/target
.idea
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/muvm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ krun-sys = { path = "../krun-sys", version = "1.9.1", default-features = false,
log = { version = "0.4.21", default-features = false, features = ["kv"] }
nix = { version = "0.29.0", default-features = false, features = ["event", "fs", "ioctl", "mman", "ptrace", "signal", "socket", "uio", "user"] }
procfs = { version = "0.17.0", default-features = false, features = [] }
rustix = { version = "0.38.34", default-features = false, features = ["fs", "mount", "process", "std", "stdio", "system", "use-libc-auxv"] }
rustix = { version = "0.38.34", default-features = false, features = ["fs", "mount", "process", "pty", "std", "stdio", "system", "termios", "use-libc-auxv"] }
serde = { version = "1.0.203", default-features = false, features = ["derive"] }
serde_json = { version = "1.0.117", default-features = false, features = ["std"] }
tempfile = { version = "3.10.1", default-features = false, features = [] }
Expand Down
8 changes: 5 additions & 3 deletions crates/muvm/src/bin/muvm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::ffi::{c_char, CString};
use std::io::Write;
use std::os::fd::{IntoRawFd, OwnedFd};
use std::path::Path;
use std::process::ExitCode;

use anyhow::{anyhow, Context, Result};
use krun_sys::{
Expand Down Expand Up @@ -59,7 +60,7 @@ fn add_ro_disk(ctx_id: u32, label: &str, path: &str) -> Result<()> {
}
}

fn main() -> Result<()> {
fn main() -> Result<ExitCode> {
env_logger::init();

if getuid().as_raw() == 0 || geteuid().as_raw() == 0 {
Expand All @@ -75,11 +76,12 @@ fn main() -> Result<()> {
options.command_args,
options.env,
options.interactive,
options.tty,
)? {
LaunchResult::LaunchRequested => {
LaunchResult::LaunchRequested(code) => {
// There was a muvm instance already running and we've requested it
// to launch the command successfully, so all the work is done.
return Ok(());
return Ok(code);
},
LaunchResult::LockAcquired {
cookie,
Expand Down
8 changes: 7 additions & 1 deletion crates/muvm/src/cli_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub struct Options {
pub fex_images: Vec<String>,
pub sommelier: bool,
pub interactive: bool,
pub tty: bool,
pub command: PathBuf,
pub command_args: Vec<String>,
}
Expand Down Expand Up @@ -110,7 +111,11 @@ pub fn options() -> OptionParser<Options> {
.switch();
let interactive = long("interactive")
.short('i')
.help("Allocate a tty guest-side and connect it to the current stdin/out")
.help("Attach to the command's stdin/out after starting it")
.switch();
let tty = long("tty")
.short('t')
.help("Allocate a tty for the command")
.switch();
let command = positional("COMMAND").help("the command you want to execute in the vm");
let command_args = any::<String, _, _>("COMMAND_ARGS", |arg| {
Expand All @@ -130,6 +135,7 @@ pub fn options() -> OptionParser<Options> {
fex_images,
sommelier,
interactive,
tty,
// positionals
command,
command_args,
Expand Down
122 changes: 70 additions & 52 deletions crates/muvm/src/launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,18 @@ 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::tty::{run_io_host, RawTerminal};
use crate::utils::launch::Launch;
use rustix::path::Arg;
use nix::unistd::unlink;
use std::ops::Range;
use std::process::{Child, Command};
use std::os::unix::net::UnixListener;
use std::process::ExitCode;

pub const DYNAMIC_PORT_RANGE: Range<u32> = 50000..50200;

pub enum LaunchResult {
LaunchRequested,
LaunchRequested(ExitCode),
LockAcquired {
cookie: Uuid,
lock_file: File,
Expand Down Expand Up @@ -56,64 +57,75 @@ impl Display for LaunchError {
}
}

fn start_socat() -> Result<(Child, u32)> {
fn acquire_socket_lock() -> Result<(File, 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('\\');
let path = socket_dir.join(format!("port-{port}.lock"));
return Ok((
if !path.exists() {
let lock_file = File::create(path).context("Failed to create socket lock")?;
flock(&lock_file, FlockOperation::NonBlockingLockExclusive)
.context("Failed to acquire socket lock")?;
lock_file
} else {
let lock_file = File::options()
.write(true)
.read(true)
.open(path)
.context("Failed to open lock file")?;
if flock(&lock_file, FlockOperation::NonBlockingLockExclusive).is_err() {
continue;
}
lock_file
},
_ => {},
}
ret.push(c);
port,
));
}
ret
Err(anyhow!("Ran out of ports."))
}

fn wrapped_launch(
server_port: u32,
cookie: Uuid,
mut command: PathBuf,
mut command_args: Vec<String>,
command: PathBuf,
command_args: Vec<String>,
env: HashMap<String, String>,
interactive: bool,
) -> Result<()> {
tty: bool,
) -> Result<ExitCode> {
if !interactive {
return request_launch(server_port, cookie, command, command_args, env);
request_launch(server_port, cookie, command, command_args, env, 0, false)?;
return Ok(ExitCode::from(0));
}
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(())
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 (_lock, vsock_port) = acquire_socket_lock()?;
let path = socket_dir.join(format!("port-{vsock_port}"));
_ = unlink(&path);
let listener = UnixListener::bind(path).context("Failed to listen on vm socket")?;
let raw_tty = if tty {
Some(
RawTerminal::set()
.context("Asked to allocate a tty for the command, but stdin is not a tty")?,
)
} else {
None
};
request_launch(
server_port,
cookie,
command,
command_args,
env,
vsock_port,
tty,
)?;
let code = run_io_host(listener, tty)?;
drop(raw_tty);
Ok(ExitCode::from(code))
}

pub fn launch_or_lock(
Expand All @@ -122,16 +134,17 @@ pub fn launch_or_lock(
command_args: Vec<String>,
env: Vec<(String, Option<String>)>,
interactive: bool,
tty: bool,
) -> Result<LaunchResult> {
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) = wrapped_launch(port, cookie, command, command_args, env, interactive) {
return Err(anyhow!("could not request launch to server: {err}"));
}
return Ok(LaunchResult::LaunchRequested);
return match wrapped_launch(port, cookie, command, command_args, env, interactive, tty) {
Err(err) => Err(anyhow!("could not request launch to server: {err}")),
Ok(code) => Ok(LaunchResult::LaunchRequested(code)),
};
}

let (lock_file, cookie) = lock_file()?;
Expand All @@ -154,6 +167,7 @@ pub fn launch_or_lock(
command_args.clone(),
env.clone(),
interactive,
tty,
) {
Err(err) => match err.downcast_ref::<LaunchError>() {
Some(&LaunchError::Connection(_)) => {
Expand All @@ -167,7 +181,7 @@ pub fn launch_or_lock(
return Err(anyhow!("could not request launch to server: {err}"));
},
},
Ok(_) => return Ok(LaunchResult::LaunchRequested),
Ok(code) => return Ok(LaunchResult::LaunchRequested(code)),
}
}
},
Expand Down Expand Up @@ -222,6 +236,8 @@ pub fn request_launch(
command: PathBuf,
command_args: Vec<String>,
env: HashMap<String, String>,
interactive_vsock: u32,
tty: bool,
) -> Result<()> {
let mut stream =
TcpStream::connect(format!("127.0.0.1:{server_port}")).map_err(LaunchError::Connection)?;
Expand All @@ -231,6 +247,8 @@ pub fn request_launch(
command,
command_args,
env,
interactive_vsock,
tty,
};

stream
Expand Down
1 change: 1 addition & 0 deletions crates/muvm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ pub mod types;

pub mod guest;
pub mod server;
pub mod tty;
pub mod utils;
4 changes: 2 additions & 2 deletions crates/muvm/src/monitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ fn set_guest_pressure(server_port: u32, cookie: Uuid, pressure: GuestPressure) -
let command = PathBuf::from("/muvmdropcaches");
let command_args = vec![];
let env = HashMap::new();
request_launch(server_port, cookie, command, command_args, env)?;
request_launch(server_port, cookie, command, command_args, env, 0, false)?;
}

let wsf: u32 = pressure.into();
Expand All @@ -53,7 +53,7 @@ fn set_guest_pressure(server_port: u32, cookie: Uuid, pressure: GuestPressure) -
let command = PathBuf::from("/sbin/sysctl");
let command_args = vec![format!("vm.watermark_scale_factor={}", wsf)];
let env = HashMap::new();
request_launch(server_port, cookie, command, command_args, env)
request_launch(server_port, cookie, command, command_args, env, 0, false)
}

fn run(server_port: u32, cookie: Uuid) {
Expand Down
Loading

0 comments on commit 38720e2

Please sign in to comment.