Skip to content

Commit af64290

Browse files
muvm: Replace socat for interactive mode.
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 <fnkl.kernel@gmail.com>
1 parent 59b4934 commit af64290

13 files changed

+470
-73
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
/target
2+
.idea

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/muvm/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ krun-sys = { path = "../krun-sys", version = "1.9.1", default-features = false,
1919
log = { version = "0.4.21", default-features = false, features = ["kv"] }
2020
nix = { version = "0.29.0", default-features = false, features = ["event", "fs", "ioctl", "mman", "ptrace", "signal", "socket", "uio", "user"] }
2121
procfs = { version = "0.17.0", default-features = false, features = [] }
22-
rustix = { version = "0.38.34", default-features = false, features = ["fs", "mount", "process", "std", "stdio", "system", "use-libc-auxv"] }
22+
rustix = { version = "0.38.34", default-features = false, features = ["fs", "mount", "process", "pty", "std", "stdio", "system", "termios", "use-libc-auxv"] }
2323
serde = { version = "1.0.203", default-features = false, features = ["derive"] }
2424
serde_json = { version = "1.0.117", default-features = false, features = ["std"] }
2525
tempfile = { version = "3.10.1", default-features = false, features = [] }

crates/muvm/src/bin/muvm.rs

+5-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::ffi::{c_char, CString};
33
use std::io::Write;
44
use std::os::fd::{IntoRawFd, OwnedFd};
55
use std::path::Path;
6+
use std::process::ExitCode;
67

78
use anyhow::{anyhow, Context, Result};
89
use krun_sys::{
@@ -59,7 +60,7 @@ fn add_ro_disk(ctx_id: u32, label: &str, path: &str) -> Result<()> {
5960
}
6061
}
6162

62-
fn main() -> Result<()> {
63+
fn main() -> Result<ExitCode> {
6364
env_logger::init();
6465

6566
if getuid().as_raw() == 0 || geteuid().as_raw() == 0 {
@@ -75,11 +76,12 @@ fn main() -> Result<()> {
7576
options.command_args,
7677
options.env,
7778
options.interactive,
79+
options.tty,
7880
)? {
79-
LaunchResult::LaunchRequested => {
81+
LaunchResult::LaunchRequested(code) => {
8082
// There was a muvm instance already running and we've requested it
8183
// to launch the command successfully, so all the work is done.
82-
return Ok(());
84+
return Ok(code);
8385
},
8486
LaunchResult::LockAcquired {
8587
cookie,

crates/muvm/src/cli_options.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub struct Options {
1818
pub fex_images: Vec<String>,
1919
pub sommelier: bool,
2020
pub interactive: bool,
21+
pub tty: bool,
2122
pub command: PathBuf,
2223
pub command_args: Vec<String>,
2324
}
@@ -110,7 +111,11 @@ pub fn options() -> OptionParser<Options> {
110111
.switch();
111112
let interactive = long("interactive")
112113
.short('i')
113-
.help("Allocate a tty guest-side and connect it to the current stdin/out")
114+
.help("Attach to the command's stdin/out after starting it")
115+
.switch();
116+
let tty = long("tty")
117+
.short('t')
118+
.help("Allocate a tty for the command")
114119
.switch();
115120
let command = positional("COMMAND").help("the command you want to execute in the vm");
116121
let command_args = any::<String, _, _>("COMMAND_ARGS", |arg| {
@@ -130,6 +135,7 @@ pub fn options() -> OptionParser<Options> {
130135
fex_images,
131136
sommelier,
132137
interactive,
138+
tty,
133139
// positionals
134140
command,
135141
command_args,

crates/muvm/src/launch.rs

+70-52
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,18 @@ use anyhow::{anyhow, Context, Result};
1111
use rustix::fs::{flock, FlockOperation};
1212
use uuid::Uuid;
1313

14-
use super::utils::env::find_in_path;
1514
use crate::env::prepare_env_vars;
15+
use crate::tty::{run_io_host, RawTerminal};
1616
use crate::utils::launch::Launch;
17-
use rustix::path::Arg;
17+
use nix::unistd::unlink;
1818
use std::ops::Range;
19-
use std::process::{Child, Command};
19+
use std::os::unix::net::UnixListener;
20+
use std::process::ExitCode;
2021

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

2324
pub enum LaunchResult {
24-
LaunchRequested,
25+
LaunchRequested(ExitCode),
2526
LockAcquired {
2627
cookie: Uuid,
2728
lock_file: File,
@@ -56,64 +57,75 @@ impl Display for LaunchError {
5657
}
5758
}
5859

59-
fn start_socat() -> Result<(Child, u32)> {
60+
fn acquire_socket_lock() -> Result<(File, u32)> {
6061
let run_path = env::var("XDG_RUNTIME_DIR")
6162
.map_err(|e| anyhow!("unable to get XDG_RUNTIME_DIR: {:?}", e))?;
6263
let socket_dir = Path::new(&run_path).join("krun/socket");
63-
let socat_path =
64-
find_in_path("socat")?.ok_or_else(|| anyhow!("Unable to find socat in PATH"))?;
6564
for port in DYNAMIC_PORT_RANGE {
66-
let path = socket_dir.join(format!("port-{}", port));
67-
if path.exists() {
68-
continue;
69-
}
70-
let child = Command::new(&socat_path)
71-
.arg(format!("unix-l:{}", path.as_os_str().to_string_lossy()))
72-
.arg("-,raw,echo=0")
73-
.spawn()?;
74-
return Ok((child, port));
75-
}
76-
Err(anyhow!("Ran out of ports."))
77-
}
78-
79-
fn escape_for_socat(s: String) -> String {
80-
let mut ret = String::with_capacity(s.len());
81-
for c in s.chars() {
82-
match c {
83-
':' | ',' | '!' | '"' | '\'' | '\\' | '(' | '[' | '{' => {
84-
ret.push('\\');
65+
let path = socket_dir.join(format!("port-{port}.lock"));
66+
return Ok((
67+
if !path.exists() {
68+
let lock_file = File::create(path).context("Failed to create socket lock")?;
69+
flock(&lock_file, FlockOperation::NonBlockingLockExclusive)
70+
.context("Failed to acquire socket lock")?;
71+
lock_file
72+
} else {
73+
let lock_file = File::options()
74+
.write(true)
75+
.read(true)
76+
.open(path)
77+
.context("Failed to open lock file")?;
78+
if flock(&lock_file, FlockOperation::NonBlockingLockExclusive).is_err() {
79+
continue;
80+
}
81+
lock_file
8582
},
86-
_ => {},
87-
}
88-
ret.push(c);
83+
port,
84+
));
8985
}
90-
ret
86+
Err(anyhow!("Ran out of ports."))
9187
}
9288

9389
fn wrapped_launch(
9490
server_port: u32,
9591
cookie: Uuid,
96-
mut command: PathBuf,
97-
mut command_args: Vec<String>,
92+
command: PathBuf,
93+
command_args: Vec<String>,
9894
env: HashMap<String, String>,
9995
interactive: bool,
100-
) -> Result<()> {
96+
tty: bool,
97+
) -> Result<ExitCode> {
10198
if !interactive {
102-
return request_launch(server_port, cookie, command, command_args, env);
99+
request_launch(server_port, cookie, command, command_args, env, 0, false)?;
100+
return Ok(ExitCode::from(0));
103101
}
104-
let (mut socat, vsock_port) = start_socat()?;
105-
command_args.insert(0, command.to_string_lossy().into_owned());
106-
command_args = vec![
107-
format!("vsock:2:{}", vsock_port),
108-
format!(
109-
"exec:{},pty,setsid,stderr",
110-
escape_for_socat(command_args.join(" "))
111-
),
112-
];
113-
command = "socat".into();
114-
request_launch(server_port, cookie, command, command_args, env)?;
115-
socat.wait()?;
116-
Ok(())
102+
let run_path = env::var("XDG_RUNTIME_DIR")
103+
.map_err(|e| anyhow!("unable to get XDG_RUNTIME_DIR: {:?}", e))?;
104+
let socket_dir = Path::new(&run_path).join("krun/socket");
105+
let (_lock, vsock_port) = acquire_socket_lock()?;
106+
let path = socket_dir.join(format!("port-{vsock_port}"));
107+
_ = unlink(&path);
108+
let listener = UnixListener::bind(path).context("Failed to listen on vm socket")?;
109+
let raw_tty = if tty {
110+
Some(
111+
RawTerminal::set()
112+
.context("Asked to allocate a tty for the command, but stdin is not a tty")?,
113+
)
114+
} else {
115+
None
116+
};
117+
request_launch(
118+
server_port,
119+
cookie,
120+
command,
121+
command_args,
122+
env,
123+
vsock_port,
124+
tty,
125+
)?;
126+
let code = run_io_host(listener, tty)?;
127+
drop(raw_tty);
128+
Ok(ExitCode::from(code))
117129
}
118130

119131
pub fn launch_or_lock(
@@ -122,16 +134,17 @@ pub fn launch_or_lock(
122134
command_args: Vec<String>,
123135
env: Vec<(String, Option<String>)>,
124136
interactive: bool,
137+
tty: bool,
125138
) -> Result<LaunchResult> {
126139
let running_server_port = env::var("MUVM_SERVER_PORT").ok();
127140
if let Some(port) = running_server_port {
128141
let port: u32 = port.parse()?;
129142
let env = prepare_env_vars(env)?;
130143
let cookie = read_cookie()?;
131-
if let Err(err) = wrapped_launch(port, cookie, command, command_args, env, interactive) {
132-
return Err(anyhow!("could not request launch to server: {err}"));
133-
}
134-
return Ok(LaunchResult::LaunchRequested);
144+
return match wrapped_launch(port, cookie, command, command_args, env, interactive, tty) {
145+
Err(err) => Err(anyhow!("could not request launch to server: {err}")),
146+
Ok(code) => Ok(LaunchResult::LaunchRequested(code)),
147+
};
135148
}
136149

137150
let (lock_file, cookie) = lock_file()?;
@@ -154,6 +167,7 @@ pub fn launch_or_lock(
154167
command_args.clone(),
155168
env.clone(),
156169
interactive,
170+
tty,
157171
) {
158172
Err(err) => match err.downcast_ref::<LaunchError>() {
159173
Some(&LaunchError::Connection(_)) => {
@@ -167,7 +181,7 @@ pub fn launch_or_lock(
167181
return Err(anyhow!("could not request launch to server: {err}"));
168182
},
169183
},
170-
Ok(_) => return Ok(LaunchResult::LaunchRequested),
184+
Ok(code) => return Ok(LaunchResult::LaunchRequested(code)),
171185
}
172186
}
173187
},
@@ -222,6 +236,8 @@ pub fn request_launch(
222236
command: PathBuf,
223237
command_args: Vec<String>,
224238
env: HashMap<String, String>,
239+
vsock_port: u32,
240+
tty: bool,
225241
) -> Result<()> {
226242
let mut stream =
227243
TcpStream::connect(format!("127.0.0.1:{server_port}")).map_err(LaunchError::Connection)?;
@@ -231,6 +247,8 @@ pub fn request_launch(
231247
command,
232248
command_args,
233249
env,
250+
vsock_port,
251+
tty,
234252
};
235253

236254
stream

crates/muvm/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ pub mod types;
1010

1111
pub mod guest;
1212
pub mod server;
13+
pub mod tty;
1314
pub mod utils;

crates/muvm/src/monitor.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ fn set_guest_pressure(server_port: u32, cookie: Uuid, pressure: GuestPressure) -
4444
let command = PathBuf::from("/muvmdropcaches");
4545
let command_args = vec![];
4646
let env = HashMap::new();
47-
request_launch(server_port, cookie, command, command_args, env)?;
47+
request_launch(server_port, cookie, command, command_args, env, 0, false)?;
4848
}
4949

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

5959
fn run(server_port: u32, cookie: Uuid) {

0 commit comments

Comments
 (0)