1use crate::{fs_util, listener, process::is_zombie};
3use anyhow::{Context, Result, bail};
4use clap::{Parser, ValueEnum};
5use nix::{
6 errno::Errno,
7 sys::{
8 signal::kill,
9 socket::{
10 AddressFamily, SockFlag, SockType, UnixAddr, connect, getsockopt, socket,
11 sockopt::PeerCredentials,
12 },
13 },
14 unistd::Pid,
15};
16use std::{
17 ffi::OsStr,
18 fmt, fs,
19 os::fd::{AsFd, AsRawFd},
20 path::{self, Path, PathBuf},
21};
22use tracing_subscriber::filter::LevelFilter;
23
24macro_rules! prefix {
25 () => {
26 "CONMON_"
27 };
28}
29
30#[derive(Clone, Debug, Eq, Parser, PartialEq)]
31#[command(
32 after_help("More info at: https://github.com/containers/conmon-rs"),
33 disable_version_flag(true)
34)]
35pub(crate) struct Config {
37 #[arg(
38 default_missing_value("default"),
39 env(concat!(prefix!(), "VERSION")),
40 long("version"),
41 num_args(0..=1),
42 short('v'),
43 value_enum,
44 value_name("VERBOSITY")
45 )]
46 version: Option<Verbosity>,
48
49 #[arg(
50 default_missing_value("default"),
51 env(concat!(prefix!(), "VERSION_JSON")),
52 long("version-json"),
53 visible_alias("vj"),
54 short('j'),
55 num_args(0..=1),
56 value_enum,
57 value_name("VERBOSITY")
58 )]
59 version_json: Option<Verbosity>,
61
62 #[arg(
63 default_value_t,
64 env(concat!(prefix!(), "LOG_LEVEL")),
65 long("log-level"),
66 short('l'),
67 value_enum,
68 value_name("LEVEL")
69 )]
70 log_level: LogLevel,
72
73 #[arg(
74 default_value_t,
75 env(concat!(prefix!(), "LOG_DRIVER")),
76 long("log-driver"),
77 short('d'),
78 value_enum,
79 value_name("DRIVER")
80 )]
81 log_driver: LogDriver,
83
84 #[arg(
87 default_value(" "),
88 env(concat!(prefix!(), "RUNTIME")),
89 hide_default_value(true),
90 long("runtime"),
91 short('r'),
92 value_name("RUNTIME")
93 )]
94 runtime: PathBuf,
97
98 #[arg(
99 default_value(" "),
100 env(concat!(prefix!(), "RUNTIME_DIR")),
101 hide_default_value(true),
102 long("runtime-dir"),
103 value_name("RUNTIME_DIR")
104 )]
105 runtime_dir: PathBuf,
108
109 #[arg(
110 env(concat!(prefix!(), "RUNTIME_ROOT")),
111 long("runtime-root"),
112 value_name("RUNTIME_ROOT")
113 )]
114 runtime_root: Option<PathBuf>,
117
118 #[arg(
119 env(concat!(prefix!(), "SKIP_FORK")),
120 long("skip-fork"),
121 value_parser = clap::builder::BoolishValueParser::new(),
123 )]
124 skip_fork: bool,
126
127 #[arg(
128 env(concat!(prefix!(), "ENABLE_TRACING")),
129 long("enable-tracing"),
130 short('e'),
131 value_parser = clap::builder::BoolishValueParser::new(),
133 )]
134 enable_tracing: bool,
137
138 #[arg(
139 default_value("http://127.0.0.1:4317"),
140 env(concat!(prefix!(), "TRACING_ENDPOINT")),
141 long("tracing-endpoint"),
142 short('t'),
143 value_name("URL")
144 )]
145 tracing_endpoint: String,
148}
149
150impl Config {
151 pub(crate) fn version(&self) -> Option<Verbosity> {
153 self.version
154 }
155
156 pub(crate) fn version_json(&self) -> Option<Verbosity> {
158 self.version_json
159 }
160
161 pub(crate) fn log_level(&self) -> LogLevel {
163 self.log_level
164 }
165
166 pub(crate) fn log_driver(&self) -> LogDriver {
168 self.log_driver
169 }
170
171 pub(crate) fn runtime(&self) -> &Path {
173 &self.runtime
174 }
175
176 pub(crate) fn runtime_is_crun(&self) -> bool {
179 self.runtime.file_name() == Some(OsStr::new("crun"))
180 }
181
182 pub(crate) fn runtime_dir(&self) -> &Path {
184 &self.runtime_dir
185 }
186
187 pub(crate) fn runtime_root(&self) -> Option<&Path> {
189 self.runtime_root.as_deref()
190 }
191
192 pub(crate) fn skip_fork(&self) -> bool {
194 self.skip_fork
195 }
196
197 pub(crate) fn enable_tracing(&self) -> bool {
199 self.enable_tracing
200 }
201
202 #[cfg(feature = "tracing")]
204 pub(crate) fn tracing_endpoint(&self) -> &str {
205 &self.tracing_endpoint
206 }
207}
208
209#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
211pub(crate) enum LogLevel {
212 Trace,
214
215 Debug,
217
218 #[default]
219 Info,
221
222 Warn,
224
225 Error,
227
228 Off,
230}
231
232#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
234pub(crate) enum Verbosity {
235 Default,
237
238 Full,
240}
241
242#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
244pub(crate) enum LogDriver {
245 None,
247
248 Stdout,
250
251 #[default]
252 Systemd,
254}
255
256impl fmt::Display for LogLevel {
259 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260 let value = self
261 .to_possible_value()
262 .expect("LogLevel has no skipped variants");
263 f.write_str(value.get_name())
264 }
265}
266
267impl From<LogLevel> for LevelFilter {
268 fn from(level: LogLevel) -> Self {
269 match level {
270 LogLevel::Trace => Self::TRACE,
271 LogLevel::Debug => Self::DEBUG,
272 LogLevel::Info => Self::INFO,
273 LogLevel::Warn => Self::WARN,
274 LogLevel::Error => Self::ERROR,
275 LogLevel::Off => Self::OFF,
276 }
277 }
278}
279
280impl Config {
281 pub(crate) fn parse_args() -> Self {
283 Self::parse()
284 }
285}
286
287const SOCKET: &str = "conmon.sock";
289const PIDFILE: &str = "pidfile";
290
291const OLD_FD_SOCKET: &str = "conmon-fd.sock";
293
294const RUNTIME_FLAG: &str = "--runtime";
296const RUNTIME_DIR_FLAG: &str = "--runtime-dir";
297const RUNTIME_ROOT_FLAG: &str = "--runtime-root";
298
299fn require_set(flag: &str, path: &Path) -> Result<()> {
302 if path
303 .to_str()
304 .with_context(|| format!("{flag} does not parse as string"))?
305 .trim()
306 .is_empty()
307 {
308 bail!("{flag} flag not set")
309 }
310 Ok(())
311}
312
313impl Config {
314 pub(crate) fn validate(&self) -> Result<()> {
316 require_set(RUNTIME_FLAG, self.runtime())?;
317 require_set(RUNTIME_DIR_FLAG, self.runtime_dir())?;
318
319 if self.runtime().file_name().is_none() {
322 bail!(
323 "{} '{}' has no file name",
324 RUNTIME_FLAG,
325 self.runtime().display()
326 )
327 }
328
329 if !self.runtime().exists() {
330 bail!(
331 "{} '{}' does not exist",
332 RUNTIME_FLAG,
333 self.runtime().display()
334 )
335 }
336
337 if let Some(rr) = self.runtime_root()
338 && rr.exists()
339 && !rr.is_dir()
340 {
341 bail!(
342 "{} '{}' is not a directory",
343 RUNTIME_ROOT_FLAG,
344 rr.display()
345 )
346 }
347
348 Ok(())
349 }
350
351 pub(crate) fn make_absolute(&mut self) -> Result<()> {
354 self.runtime = path::absolute(&self.runtime).context("make runtime path absolute")?;
355 self.runtime_dir =
356 path::absolute(&self.runtime_dir).context("make runtime dir absolute")?;
357 if let Some(rr) = &mut self.runtime_root {
358 *rr = path::absolute(&*rr).context("make runtime root absolute")?;
359 }
360 Ok(())
361 }
362
363 pub(crate) fn prepare(&self) -> Result<()> {
366 fs::create_dir_all(self.runtime_dir()).context("create runtime dir")?;
367
368 if let Some(rr) = self.runtime_root() {
369 fs::create_dir_all(rr).context("create runtime root")?;
370 }
371
372 let socket = self.socket();
376 if Self::socket_in_use(&socket)
377 .with_context(|| format!("check socket {}", socket.display()))?
378 {
379 bail!(listener::SocketInUse(format!(
380 "socket {} is in use by a running server",
381 socket.display()
382 )))
383 }
384 fs_util::remove_file(&socket)
385 .with_context(|| format!("remove stale socket {}", socket.display()))?;
386
387 let fd_socket = self.runtime_dir().join(OLD_FD_SOCKET);
391 if let Err(e) = fs_util::remove_file(&fd_socket) {
392 eprintln!(
393 "Unable to remove the stale socket {}: {e:#}",
394 fd_socket.display()
395 );
396 }
397
398 Ok(())
399 }
400
401 fn socket_in_use(path: &Path) -> Result<bool> {
406 let (short_path, _dir) = listener::shorten_path(path)?;
408 let fd = socket(
409 AddressFamily::Unix,
410 SockType::Stream,
411 SockFlag::SOCK_CLOEXEC | SockFlag::SOCK_NONBLOCK,
412 None,
413 )
414 .context("create socket")?;
415 let addr = UnixAddr::new(&short_path).context("create socket address")?;
416 Ok(match connect(fd.as_raw_fd(), &addr) {
417 Ok(()) => Self::peer_running(&fd),
418 Err(Errno::EAGAIN) => true,
419 Err(Errno::ECONNREFUSED | Errno::ENOENT | Errno::ENOTSOCK | Errno::EPROTOTYPE) => false,
422 Err(Errno::EACCES) => {
426 eprintln!(
427 "Replacing socket {} which is not accessible",
428 path.display()
429 );
430 false
431 }
432 Err(e) => return Err(e).context("connect socket"),
434 })
435 }
436
437 fn peer_running(fd: &impl AsFd) -> bool {
440 let Ok(credentials) = getsockopt(fd, PeerCredentials) else {
441 return true;
442 };
443 let pid = credentials.pid();
444 if pid <= 0 {
446 return true;
447 }
448 let pid = Pid::from_raw(pid);
450 kill(pid, None) != Err(Errno::ESRCH) && !is_zombie(pid)
451 }
452
453 pub(crate) fn socket(&self) -> PathBuf {
455 self.runtime_dir().join(SOCKET)
456 }
457
458 pub(crate) fn conmon_pidfile(&self) -> PathBuf {
460 self.runtime_dir().join(PIDFILE)
461 }
462}
463
464impl Config {
465 #[cfg(test)]
469 pub(crate) fn parse_for_test<I, T>(args: I) -> Self
470 where
471 I: IntoIterator<Item = T>,
472 T: Into<std::ffi::OsString> + Clone,
473 {
474 use clap::{CommandFactory, FromArgMatches};
475 let without_env = |arg: clap::Arg| arg.env(None);
476 let matches = <Self as CommandFactory>::command()
477 .mut_args(without_env)
478 .try_get_matches_from(
479 std::iter::once("conmonrs".into()).chain(args.into_iter().map(Into::into)),
480 )
481 .expect("parse args");
482 Self::from_arg_matches(&matches).expect("parse args")
483 }
484}
485
486#[cfg(test)]
487mod tests;