Repository: sorah/subsystemctl Branch: master Commit: cf112a287313 Files: 10 Total size: 31.9 KB Directory structure: gitextract_nwmrkkw_/ ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── rustfmt.toml └── src/ ├── bottle.rs ├── environment.rs ├── error.rs ├── lib.rs └── main.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /target ================================================ FILE: Cargo.toml ================================================ [package] name = "subsystemctl" version = "0.2.0" description = "Utility to run systemd in WSL2 with a Linux namespace" homepage = "https://github.com/sorah/subsystemctl" repository = "https://github.com/sorah/subsystemctl" authors = ["Sorah Fukumori "] license = "MIT" edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] anyhow = "1.0.31" thiserror = "1.0.19" clap = "2.33.1" nix = "0.17.0" libc = "0.2.71" env_logger = "0.7.1" log = "0.4.8" ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Sorah Fukumori Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # subsystemd: Run systemd in WSL2 __Maintanence Note:__ You may want to use [nullpo-head/wsl-distrod](https://github.com/nullpo-head/wsl-distrod) which provides better compatibility with systemd and easier installation, and well maintained. Run systemd under Linux namespace in WSL2. Heavily inspired by [arkane-systems/genie][genie], but written in Rust. ## Difference with arkane-systems/genie Slightly following [genie]'s behavior, but noted below... - Interface - Command line interface is not compatible. - Behavior - Hostname altertion is optional with `--hostname`, `--hostname-suffix` - `/etc/hosts` are not updated. Users are encouraged to use `nss-myhostname`. - Uses `machinectl shell` to launch a user shell; this allows running systemd user session - Internal - Removed dependency to `unshare`, `daemonize`, `nsenter` command line tools - systemd-wide environment variables are set via `systemd.conf` drop-in, using `DefaultEnvironment=` - systemd PID from root namespace is stored at `/run/subsystemctl/systemd.pid` ## Install ### Arch Linux PKGBUILD: https://github.com/sorah/arch.sorah.jp/tree/master/aur-sorah/PKGBUILDs/subsystemctl _(PKGBUILD originally submitted to AUR (https://aur.archlinux.org/packages/subsystemctl) was deleted as [they unwelcomes WSL-exclusive packages](https://lists.archlinux.org/pipermail/aur-requests/2020-June/041193.html).)_ ### Debian/Ubuntu Refer to https://github.com/nkmideb/subsystemctl for debian source. Pre-built package binaries available at https://github.com/nkmideb/subsystemctl/releases for your convenient. ### Self build ``` cargo install subsystemctl ``` or from git source: ```bash cargo build --release install -m6755 -oroot -groot ./target/release/subsystemctl /usr/local/bin/subsystemctl ``` ## Usage ### `subsystemctl start`: Start `systemd` environment ```ps1 PS> wsl -u root -- subsystemctl start ``` ### `subsystemctl shell`: shell login to systemd-enabled environment ```ps1 PS> wsl subsystemctl shell Connected to the local host. Press ^] three times within 1s to exit session. someone@hostname$ ... ``` #### Specifying uid to login ```ps1 PS> wsl -u root -- subsystemctl shell --uid=1000 Connected to the local host. Press ^] three times within 1s to exit session. someone@hostname$ ... ``` #### Automatically starting and entering a user shell ```ps1 PS> wsl -u root -d Arch -- subsystemctl shell --uid=1000 --start [2021-06-27T16:32:20Z INFO subsystemctl] Starting systemd Connected to the local host. Press ^] three times within 1s to exit session. someone@hostname$ ... ``` ### `subsystemctl exec`: Raw `nsenter` like interface ```ps1 PS> wsl subsystemctl exec id uid=1000(sorah) gid=1000(sorah) groups=1000(sorah),116(admin) ``` #### Specifying uid (and gid) ```ps1 PS> wsl -u root -- subsystemctl exec id uid=0(root) gid=0(root) groups=0(root) PS> wsl -u root -- subsystemctl exec --uid=1000 id uid=1000(sorah) gid=1000(sorah) groups=1000(sorah),116(admin) PS> wsl -u root -- subsystemctl exec --uid=1000 --gid=116 id uid=1000(sorah) gid=116(admin) groups=116(admin) ``` ### `subsystemctl is-running` ```bash #!/bin/bash if subsystemctl is-running; then echo "running" else echo "not-running" fi ``` ### `subsystemctl is-inside` ```bash #!/bin/bash if subsystemctl is-inside; then echo "inside" else echo "outside" fi ``` ## Tips ### systemd-resolved, networkd are recommended to be disabled otherwise `/etc/resolv.conf` might get overwritten to resolved stub-resolver. ## Author Sorah Fukumori https://sorah.jp/ ## License MIT [genie]: https://github.com/arkane-systems/genie ================================================ FILE: rustfmt.toml ================================================ max_width = 110 use_small_heuristics = "Max" newline_style = "Unix" ================================================ FILE: src/bottle.rs ================================================ use crate::environment; use crate::error; use std::ffi::CStr; use std::ffi::CString; use std::ffi::OsStr; static RUNTIME_DIR: &str = "/run/subsystemctl"; static PID_FILE: &str = "/run/subsystemctl/systemd.pid"; static HOSTNAME_FILE: &str = "/run/subsystemctl/hostname"; static ORIG_HOSTNAME_FILE: &str = "/run/subsystemctl/hostname.orig"; const OS_NONE: Option<&'static [u8]> = None; pub fn get_systemd_pid() -> Result, Box> { let result = std::fs::read(PID_FILE); match result { Ok(buf) => { let pid: i32 = String::from_utf8(buf)?.trim().parse()?; Ok(Some(pid)) } Err(e) => { if e.kind() == std::io::ErrorKind::NotFound { Ok(None) } else { Err(Box::new(e)) } } } } fn put_systemd_pid(pid: i32) -> std::io::Result<()> { std::fs::create_dir_all(RUNTIME_DIR)?; std::fs::write(PID_FILE, format!("{}\n", pid)) } fn zap_systemd_pid() -> std::io::Result<()> { if std::fs::metadata(PID_FILE).is_ok() { std::fs::remove_file(PID_FILE)?; } Ok(()) } pub fn get_original_hostname() -> std::io::Result { let buf = if std::fs::metadata(ORIG_HOSTNAME_FILE).is_ok() { std::fs::read(ORIG_HOSTNAME_FILE) } else { std::fs::read("/etc/hostname") }?; Ok(String::from_utf8(buf).unwrap().trim().to_owned()) } pub fn put_hostname(name: String) -> std::io::Result<()> { std::fs::create_dir_all(RUNTIME_DIR)?; if std::fs::metadata(ORIG_HOSTNAME_FILE).is_err() { let orig_hostname = std::fs::read("/etc/hostname")?; std::fs::write(ORIG_HOSTNAME_FILE, orig_hostname)? } std::fs::write(HOSTNAME_FILE, format!("{}\n", name)) } pub fn is_running() -> bool { if environment::is_pid1_systemd() { return true; } if let Ok(pid_o) = get_systemd_pid() { if let Some(pid) = pid_o { if let Ok(meta) = std::fs::metadata(std::format!("/proc/{}", pid)) { return meta.is_dir(); } } } false } pub fn is_inside() -> bool { environment::is_pid1_systemd() } pub fn start(name: Option) -> Result<(), Box> { if let Some(hostname) = name { ensure_hostname(hostname)?; } exec_systemd_ensure_dropin()?; Ok(exec_systemd()?) } fn exec_systemd_ensure_dropin() -> std::io::Result<()> { std::fs::create_dir_all("/run/systemd/system.conf.d")?; let envs = vec!["WSL_INTEROP", "WSL_DISTRO_NAME", "WSL_NAME", "WT_SESSION", "WT_PROFILE_ID"]; let envvar_strs: Vec = envs .into_iter() .filter_map(|name| { let e = std::env::var(name); if let Ok(env) = e { if env.contains("\"") { None // XXX: } else { Some(format!("\"{}={}\"", name, env)) } } else { None } }) .collect(); let dropin = format!( "[Manager]\nDefaultEnvironment=INSIDE_GENIE=1 INSIDE_SUBSYSTEMCTL=1 {}\n", envvar_strs.join(" ") ); std::fs::write("/run/systemd/system.conf.d/10-subsystemctl-env.conf", dropin) } fn ensure_hostname(name: String) -> Result<(), Box> { let needs_bind = std::fs::metadata(HOSTNAME_FILE).is_err(); put_hostname(name)?; if !needs_bind { return Ok(()); } nix::mount::mount( Some(OsStr::new(HOSTNAME_FILE)), OsStr::new("/etc/hostname"), OS_NONE, nix::mount::MsFlags::MS_BIND, OS_NONE, )?; Ok(()) } fn exec_systemd() -> Result<(), error::Error> { use nix::unistd::ForkResult; let systemd_bin = CString::new(environment::systemd_bin().unwrap()).unwrap(); match nix::unistd::fork() { Ok(ForkResult::Child) => exec_systemd0_handle_child_failure(exec_systemd1_child(systemd_bin)), Ok(ForkResult::Parent { child, .. }) => exec_systemd1_parent(child), Err(e) => panic!("{}",e), } } fn exec_systemd0_handle_child_failure(r: Result<(), error::Error>) -> Result<(), error::Error> { match r { Ok(_) => {} // do nothing Err(error::Error::StartFailed(exitstatus)) => { log::error!("Something went wrong while starting"); std::process::exit(exitstatus); } Err(e) => panic!("{}",e), } std::process::exit(0); } fn exec_systemd1_parent(child: nix::unistd::Pid) -> Result<(), error::Error> { use nix::sys::wait::WaitStatus; // TODO: monitor systemd status instead of pid file loop { match get_systemd_pid() { Ok(Some(pid)) => { log::debug!("Watching pid: child_pid={}, pid={}", child, pid); let pidns_path = format!("/proc/{}/ns/pid", pid); let mntns_path = format!("/proc/{}/ns/mnt", pid); if std::fs::metadata(pidns_path).is_ok() && std::fs::metadata(mntns_path).is_ok() { break; } } Ok(None) => { log::debug!("Watching pid: none"); } Err(e) => { log::debug!("Watching pid: e={:?}", e); } } match nix::sys::wait::waitpid(child, Some(nix::sys::wait::WaitPidFlag::WNOWAIT)) { Ok(WaitStatus::Exited(_pid, status)) => { return Err(error::Error::StartFailed(status)); } Ok(WaitStatus::Signaled(_pid, signal, _)) => { return Err(error::Error::StartFailed(128 + (signal as i32))); } Err(nix::Error::Sys(nix::errno::Errno::ECHILD)) => { return Err(error::Error::StartFailed(128)); } _ => {} // ignore, } std::thread::sleep(std::time::Duration::from_millis(500)); } // TODO: Ok(()) } fn exec_systemd1_child(systemd_bin: CString) -> Result<(), error::Error> { use nix::sched::CloneFlags; use nix::unistd::ForkResult; nix::sched::unshare(CloneFlags::CLONE_NEWNS | CloneFlags::CLONE_NEWPID).unwrap(); nix::unistd::setsid().unwrap(); match nix::unistd::fork() { Ok(ForkResult::Child) => exec_systemd0_handle_child_failure(exec_systemd2_child(systemd_bin)), Ok(ForkResult::Parent { child, .. }) => exec_systemd2_parent(child), Err(e) => panic!("{}",e), } } fn exec_systemd2_parent(child: nix::unistd::Pid) -> Result<(), error::Error> { put_systemd_pid(child.as_raw()).unwrap(); std::process::exit(0); } fn exec_systemd2_child(systemd_bin: CString) -> Result<(), error::Error> { use nix::fcntl::OFlag; use nix::mount::MsFlags; use std::os::unix::io::RawFd; nix::mount::mount( Some(OsStr::new("none")), OsStr::new("/"), OS_NONE, MsFlags::MS_REC | MsFlags::MS_SHARED, OS_NONE, ) .expect("set_propagation mount failure"); nix::mount::mount( Some(OsStr::new("none")), OsStr::new("/proc"), OS_NONE, MsFlags::MS_REC | MsFlags::MS_PRIVATE, OS_NONE, ) .expect("proc propagation mount failure"); nix::mount::mount( Some(OsStr::new("proc")), OsStr::new("/proc"), Some(OsStr::new("proc")), MsFlags::MS_NOSUID | MsFlags::MS_NODEV | MsFlags::MS_NOEXEC, OS_NONE, ) .expect("proc mount failure"); nix::unistd::chdir("/").unwrap(); nix::unistd::setgid(nix::unistd::Gid::from_raw(0)).unwrap(); nix::unistd::setuid(nix::unistd::Uid::from_raw(0)).unwrap(); match nix::unistd::close(0 as RawFd) { Ok(_) => {} Err(nix::Error::Sys(nix::errno::Errno::EBADF)) => {} Err(e) => panic!("{}",e), } match nix::unistd::close(1 as RawFd) { Ok(_) => {} Err(nix::Error::Sys(nix::errno::Errno::EBADF)) => {} Err(e) => panic!("{}",e), } match nix::unistd::close(2 as RawFd) { Ok(_) => {} Err(nix::Error::Sys(nix::errno::Errno::EBADF)) => {} Err(e) => panic!("{}",e), } nix::fcntl::open(OsStr::new("/dev/null"), OFlag::O_RDONLY, nix::sys::stat::Mode::empty()).unwrap(); nix::fcntl::open(OsStr::new("/dev/null"), OFlag::O_WRONLY, nix::sys::stat::Mode::empty()).unwrap(); nix::fcntl::open(OsStr::new("/dev/null"), OFlag::O_WRONLY, nix::sys::stat::Mode::empty()).unwrap(); nix::unistd::execve(systemd_bin.as_c_str(), &[systemd_bin.as_c_str()], &[]).unwrap(); panic!("should unreach"); } pub fn stop() -> Result<(), Box> { let systemd_pid = get_systemd_pid().unwrap().unwrap(); nix::sys::signal::kill(nix::unistd::Pid::from_raw(systemd_pid), Some(SIGRTMIN_plus_4()))?; zap_systemd_pid().unwrap(); Ok(()) } extern "C" { fn __libc_current_sigrtmin() -> libc::c_int; } #[allow(non_snake_case)] fn SIGRTMIN_plus_4() -> nix::sys::signal::Signal { unsafe { std::mem::transmute(__libc_current_sigrtmin() + 4) } } pub fn wait() -> Result<(), Box> { use nix::sys::wait::WaitStatus; use nix::unistd::ForkResult; log::debug!("Waiting systemd-machined to start"); let machinectl = environment::machinectl_bin()?; match nix::unistd::fork() { Ok(ForkResult::Child) => { wait_internal(machinectl); std::process::exit(0); } Ok(ForkResult::Parent { child, .. }) => { loop { match nix::sys::wait::waitpid(child, None) { Ok(WaitStatus::Exited(_pid, status)) => { if status == 0 { log::debug!("machined is now up"); return Ok(()); } else { return Err(Box::new(error::Error::WaitFailed)); } } Ok(WaitStatus::Signaled(_pid, _signal, _)) => { return Err(Box::new(error::Error::WaitFailed)); } Ok(_) => {} // ignore Err(nix::Error::Sys(nix::errno::Errno::ECHILD)) => return Ok(()), // ??? Err(nix::Error::Sys(nix::errno::Errno::EINTR)) => {} // ignore Err(e) => panic!("{}",e), } } } Err(e) => panic!("{}",e), } } fn wait_internal(machinectl: String) { setns_systemd(); loop { let cmd = std::process::Command::new(&machinectl) .arg("list") .output() .expect("failed to execute machinectl list"); if cmd.status.success() { break; } std::thread::sleep(std::time::Duration::from_millis(600)); log::debug!("Still waiting systemd-machined to start"); } log::debug!("systemd-machined is up (internal)"); } pub fn exec(cmdline: Vec, uid: nix::unistd::Uid, gid: nix::unistd::Gid) -> Result { let args_string: Vec = cmdline.into_iter().map(|a| CString::new(a).unwrap()).collect(); let args: Vec<&CStr> = args_string.iter().map(|s| s.as_c_str()).collect(); enter(args[0], args.as_slice(), uid, gid) } pub fn shell(uid: Option, quiet: bool) -> Result { let machinectl = CString::new(environment::machinectl_bin()?).unwrap(); let mut args = vec![CString::new("machinectl").unwrap(), CString::new("shell").unwrap()]; if let Some(u) = uid { args.push(CString::new("--uid").unwrap()); args.push(CString::new(format!("{}", u)).unwrap()); } if quiet { args.push(CString::new("--quiet").unwrap()); } args.push(CString::new("--setenv").unwrap()); args.push(CString::new(format!("SUBSYSTEMCTL_PATH={}", std::env::current_dir().unwrap().display())).unwrap()); args.push(CString::new(".host").unwrap()); args.push(CString::new("/bin/sh").unwrap()); args.push(CString::new("-c").unwrap()); args.push(CString::new("cd \"${SUBSYSTEMCTL_PATH}\"; exec ${SHELL:-sh}").unwrap()); let args_c: Vec<&CStr> = args.iter().map(|s| s.as_c_str()).collect(); enter( machinectl.as_c_str(), &args_c.as_slice(), nix::unistd::Uid::from_raw(0), nix::unistd::Gid::from_raw(0), ) } fn setns_systemd() { use nix::fcntl::OFlag; use nix::sched::CloneFlags; let sd_pid = get_systemd_pid().unwrap().unwrap(); let pidns_path = format!("/proc/{}/ns/pid", sd_pid); let mntns_path = format!("/proc/{}/ns/mnt", sd_pid); { let pidns_fd = nix::fcntl::open(OsStr::new(&pidns_path), OFlag::O_RDONLY, nix::sys::stat::Mode::empty()) .unwrap(); nix::sched::setns(pidns_fd, CloneFlags::CLONE_NEWPID).unwrap(); nix::unistd::close(pidns_fd).unwrap(); } { let mntns_fd = nix::fcntl::open(OsStr::new(&mntns_path), OFlag::O_RDONLY, nix::sys::stat::Mode::empty()) .unwrap(); nix::sched::setns(mntns_fd, CloneFlags::CLONE_NEWNS).unwrap(); nix::unistd::close(mntns_fd).unwrap(); } } fn enter( path: &CStr, args: &[&CStr], uid: nix::unistd::Uid, gid: nix::unistd::Gid, ) -> Result { use nix::sys::wait::WaitStatus; use nix::unistd::ForkResult; setns_systemd(); match nix::unistd::fork() { Ok(ForkResult::Child) => { log::debug!("enter(child): uid={}, gid={}", uid, gid); nix::unistd::setgroups(&[]).unwrap(); unsafe { let ent = libc::getpwuid(uid.as_raw() as libc::uid_t); if !ent.is_null() { let username = CString::from_raw((*ent).pw_name); nix::unistd::initgroups(&username, gid).unwrap(); } } nix::unistd::setgid(gid).unwrap(); nix::unistd::setuid(uid).unwrap(); log::debug!("execvp {:?}, {:?}", path, args); match nix::unistd::execvp(path, args) { Err(nix::Error::Sys(errno)) => { log::error!("exec failed: {}", errno.desc()); return Ok(128); } Err(e) => panic!("{}",e), Ok(_) => {} } panic!("should unreach"); } Ok(ForkResult::Parent { child, .. }) => { loop { match nix::sys::wait::waitpid(child, None) { Ok(WaitStatus::Exited(_pid, status)) => { return Ok(status); } Ok(WaitStatus::Signaled(_pid, signal, _)) => { return Ok(128 + (signal as i32)); } Ok(_) => {} // ignore Err(nix::Error::Sys(nix::errno::Errno::ECHILD)) => return Ok(128), // ??? Err(nix::Error::Sys(nix::errno::Errno::EINTR)) => {} // ignore Err(e) => panic!("{}",e), } } } Err(e) => panic!("{}",e), } } ================================================ FILE: src/environment.rs ================================================ use crate::error::Error; static PROC_MOUNTS: &str = "/proc/self/mounts"; static OSRELEASE_FILE: &str = "/proc/sys/kernel/osrelease"; static WSL_OSRELEASE: &str = "microsoft"; pub fn is_wsl() -> Result> { let osrelease = String::from_utf8(std::fs::read(OSRELEASE_FILE)?)?; Ok(osrelease.contains(WSL_OSRELEASE)) } pub fn is_wsl1() -> Result> { log::debug!("Checking is_wsl1"); // Assume we're in WSL 1 or 2. // Find rootfs is lxfs or not. If it's lxfs, then it's WSL1, otherwise considered 2. let mounts_buf = String::from_utf8(std::fs::read(PROC_MOUNTS)?)?; let mut mounts_lines = mounts_buf.lines(); while let Some(mount) = mounts_lines.next() { let mut iter = mount.split_ascii_whitespace(); iter.next(); let mountpoint_o = iter.next(); let fstype_o = iter.next(); if let (Some(mountpoint), Some(fstype)) = (mountpoint_o, fstype_o) { log::debug!("Checking is_wsl1: mountpoint={}, fstype={}", mountpoint, fstype); if mountpoint != "/" { continue; } return Ok(fstype == "lxfs" || fstype == "wslfs"); } else { return Err(Box::new(Error::InvalidProcMounts)); } } Ok(false) } pub fn systemd_bin() -> Result { //if let Ok(bin) = std::env::var("SUBSYSTEMCTL_SYSTEMD_BIN") { // return Ok(bin); //} if std::fs::metadata("/lib/systemd/systemd").is_ok() { return Ok("/lib/systemd/systemd".to_string()); } if std::fs::metadata("/usr/lib/systemd/systemd").is_ok() { return Ok("/usr/lib/systemd/systemd".to_string()); } Err(Error::NoSystemdFound) } pub fn machinectl_bin() -> Result { // if let Ok(bin) = std::env::var("SUBSYSTEMCTL_MACHINECTL_BIN") { // return Ok(bin) // } if std::fs::metadata("/usr/bin/machinectl").is_ok() { return Ok("/usr/bin/machinectl".to_string()); } if std::fs::metadata("/bin/machinectl").is_ok() { return Ok("/bin/machinectl".to_string()); } Err(Error::NoSystemdFound) } pub fn is_pid1_systemd() -> bool { if let Ok(cmdline_vec) = std::fs::read("/proc/1/cmdline") { let cmdline = String::from_utf8(cmdline_vec).unwrap(); let binpath = cmdline.split('\0').next().unwrap(); binpath == systemd_bin().unwrap() } else { false } } ================================================ FILE: src/error.rs ================================================ #[derive(thiserror::Error, Debug)] pub enum Error { #[error("Unexpected format in /proc/self/mounts")] InvalidProcMounts, #[error("Systemd not found in standard locations (machinectl and systemd are necessary)")] NoSystemdFound, #[error("Systemd is not running")] NotRunning, #[error("Something went wrong while waiting")] WaitFailed, #[error("Something went wrong while starting, exitstatus={0}")] StartFailed(i32), } ================================================ FILE: src/lib.rs ================================================ pub mod environment; pub mod error; pub mod bottle; ================================================ FILE: src/main.rs ================================================ use subsystemctl::bottle; use subsystemctl::environment; fn main() -> anyhow::Result<()> { env_logger::from_env(env_logger::Env::default().default_filter_or("info")).init(); let app = clap::App::new(clap::crate_name!()) .version(clap::crate_version!()) .about(clap::crate_description!()) .setting(clap::AppSettings::SubcommandRequired) .subcommand( clap::SubCommand::with_name("start") .about("Start systemd in a Linux namespace (mount, pid)") .arg( clap::Arg::with_name("hostname") .help("Change hostname during start up") .takes_value(true) .short("n") .long("hostname"), ) .arg( clap::Arg::with_name("hostname-suffix") .help("Append a suffix to hostname during startup") .takes_value(true) .short("N") .long("hostname-suffix"), ) .arg( clap::Arg::with_name("wait") .help("Wait systemd-machined to start up") .short("w") .long("wait"), ), ) .subcommand(clap::SubCommand::with_name("stop").about("stop")) .subcommand( clap::SubCommand::with_name("shell") .about("Start a shell in a systemd namespace using machinectl-shell") .arg( clap::Arg::with_name("start") .help("Start systemd when necessary") .short("s") .long("start"), ) .arg( clap::Arg::with_name("quiet") .help("Suppress machinectl-shell output") .short("q") .long("quiet"), ) .arg(clap::Arg::with_name("uid").takes_value(true).short("u").long("uid")), ) .subcommand( clap::SubCommand::with_name("exec") .about("Execute a command in a systemd namespace") .arg( clap::Arg::with_name("start") .help("Start systemd when necessary") .short("s") .long("start"), ) .arg( clap::Arg::with_name("uid") .help("setuid(2) on exec. Only available for root, default to current uid (getuid(2)") .takes_value(true) .short("u") .long("uid"), ) .arg( clap::Arg::with_name("gid") .help("setgid(2) on exec. Only available for root, default to current gid (getgid(2)") .takes_value(true) .short("g") .long("gid"), ) .arg(clap::Arg::with_name("command").takes_value(true).multiple(true) .allow_hyphen_values(true) .last(true)), ) .subcommand( clap::SubCommand::with_name("is-running") .about("Return 0 if a systemd namespace is running, otherwise 1"), ) .subcommand( clap::SubCommand::with_name("is-inside") .about("Return 0 if invoked from inside of a systemd namespace, otherwise 1"), ); let matches = app.get_matches(); run_subcommand(matches.subcommand()) } fn run_subcommand(subcommand: (&str, Option<&clap::ArgMatches>)) -> anyhow::Result<()> { match subcommand { ("start", Some(m)) => cmd_start(m), ("stop", Some(m)) => cmd_stop(m), ("exec", Some(m)) => cmd_exec(m), ("shell", Some(m)) => cmd_shell(m), ("is-running", Some(m)) => cmd_is_running(m), ("is-inside", Some(m)) => cmd_is_inside(m), _ => panic!("?"), } } fn cmd_start(m: &clap::ArgMatches) -> anyhow::Result<()> { check_root()?; check_prereq()?; if bottle::is_running() { log::warn!("systemd is running, not starting again"); return Ok(()); } let hostname = if m.is_present("hostname") { Some(m.value_of_lossy("hostname").unwrap().to_string()) } else if m.is_present("hostname-suffix") { let suffix = m.value_of_lossy("hostname-suffix").unwrap(); Some(std::format!("{}{}", bottle::get_original_hostname()?, suffix)) } else { None }; // TODO: resolv.conf autostart(m.is_present("wait"), hostname)?; Ok(()) } fn cmd_stop(_m: &clap::ArgMatches) -> anyhow::Result<()> { check_root()?; check_prereq()?; if !bottle::is_running() { log::warn!("systemd is already stopped or not running"); return Ok(()); } if bottle::is_inside() { return Err(anyhow::anyhow!("Cannot stop from inside of systemd environment")); } let r = bottle::stop(); if let Err(e) = r { return Err(anyhow::anyhow!("Failed to stop: {}", e)); } Ok(()) } fn cmd_exec(m: &clap::ArgMatches) -> anyhow::Result<()> { check_prereq()?; if !bottle::is_running() { if m.is_present("start") { log::info!("Starting systemd"); autostart(true, None)?; } else { return Err(anyhow::anyhow!("systemd is not running. Try start it first: subsystemctl start")); } } let cmd = m.values_of_lossy("command"); if cmd.is_none() { return Err(anyhow::anyhow!("command not given")); } let (uid, gid) = extract_uid_gid(m)?; let r = bottle::exec(cmd.unwrap(), uid, gid); match r { Ok(retval) => std::process::exit(retval), Err(e) => return Err(anyhow::anyhow!("Failed to start: {}", e)), } } fn cmd_shell(m: &clap::ArgMatches) -> anyhow::Result<()> { check_prereq()?; if !bottle::is_running() { if m.is_present("start") { log::info!("Starting systemd"); autostart(true, None)?; } else { return Err(anyhow::anyhow!("systemd is not running. Try start it first: subsystemctl start")); } } let (uid, _gid) = extract_uid_gid(m)?; let quiet = m.is_present("quiet"); let r = bottle::shell(Some(uid), quiet); match r { Ok(retval) => std::process::exit(retval), Err(e) => return Err(anyhow::anyhow!("Failed to start: {}", e)), } } fn cmd_is_running(_m: &clap::ArgMatches) -> anyhow::Result<()> { check_prereq()?; if !bottle::is_running() { std::process::exit(1); } Ok(()) } fn cmd_is_inside(_m: &clap::ArgMatches) -> anyhow::Result<()> { check_prereq()?; if !bottle::is_inside() { std::process::exit(1); } Ok(()) } fn check_prereq() -> anyhow::Result<()> { if std::env::var("SUBSYSTEMCTL_IGNORE_WSL_CHECK").is_err() { if !environment::is_wsl().expect("Cannot check WSL1/2 status") { return Err(anyhow::anyhow!("not running in WSL1/2; This tool only runs in WSL2")); } if environment::is_wsl1().expect("Cannot check WSL1") { return Err(anyhow::anyhow!("not running in WSL2; This tool only runs in WSL2")); } } Ok(()) } fn check_root() -> anyhow::Result<()> { if !nix::unistd::getuid().is_root() { return Err(anyhow::anyhow!("This subcommand is only available for root")); } Ok(()) } fn extract_uid_gid(m: &clap::ArgMatches) -> anyhow::Result<(nix::unistd::Uid, nix::unistd::Gid)> { if !nix::unistd::getuid().is_root() { if m.is_present("uid") || m.is_present("gid") { return Err(anyhow::anyhow!("uid,gid flags are only available for root")); } log::debug!( "extract_uid_gid: non-root, uid={}, gid={}", nix::unistd::getuid(), nix::unistd::getgid() ); return Ok((nix::unistd::getuid(), nix::unistd::getgid())); } let uid = if let Some(id) = m.value_of("uid") { log::debug!("uid flag: {}", &id); nix::unistd::Uid::from_raw(id.parse()?) } else { nix::unistd::Uid::from_raw(0) }; let gid = if let Some(id) = m.value_of("gid") { log::debug!("gid flag: {}", &id); nix::unistd::Gid::from_raw(id.parse()?) } else { if !uid.is_root() { nix::unistd::Gid::from_raw(uid.as_raw()) } else { nix::unistd::getgid() } }; Ok((uid, gid)) } fn autostart(wait: bool, hostname: Option) -> anyhow::Result<()> { environment::machinectl_bin()?; let r = bottle::start(hostname); if let Err(e) = r { return Err(anyhow::anyhow!("Failed to start: {}", e)); } if wait { if let Err(e) = bottle::wait() { return Err(anyhow::anyhow!("Failed to wait machined: {}", e)); } } Ok(()) }