1use std::process::Command;
7
8use anyhow::{Context, Result};
9
10pub fn is_available() -> bool {
12 which::which("systemctl").is_ok()
13}
14
15pub fn enable_linger() -> Result<()> {
17 let whoami = std::env::var("USER").unwrap_or_default();
18 if whoami.is_empty() || which::which("loginctl").is_err() {
19 return Ok(());
20 }
21 let mut cmd = Command::new("loginctl");
22 cmd.args(["enable-linger", &whoami]);
23 let output = cmd
24 .stdout(std::process::Stdio::piped())
25 .stderr(std::process::Stdio::piped())
26 .spawn()
27 .context("failed to spawn loginctl")?
28 .wait_with_output()
29 .context("loginctl command failed")?;
30 if !output.status.success() {
31 let stderr = String::from_utf8_lossy(&output.stderr);
32 eprintln!("Warning: enable-linger failed: {stderr}");
33 } else {
34 println!("Linger enabled for user.");
35 }
36 Ok(())
37}
38
39pub fn daemon_reload() -> Result<()> {
41 if !is_available() {
42 return Ok(());
43 }
44 let mut cmd = Command::new("systemctl");
45 cmd.args(["--user", "daemon-reload"]);
46 let output = cmd
47 .stdout(std::process::Stdio::piped())
48 .stderr(std::process::Stdio::piped())
49 .spawn()
50 .context("failed to spawn systemctl daemon-reload")?
51 .wait_with_output()
52 .context("systemctl daemon-reload failed")?;
53 if !output.status.success() {
54 let stderr = String::from_utf8_lossy(&output.stderr);
55 anyhow::bail!("daemon-reload failed: {}", stderr.trim());
56 }
57 Ok(())
58}
59
60pub fn reset_failed(name: &str) -> Result<()> {
62 if !is_available() {
63 return Ok(());
64 }
65 let unit_names = [
66 format!("{name}.service"),
67 format!("{name}.socket"),
68 format!("{name}-host.service"),
69 format!("{name}-proxy.service"),
70 format!("{name}-compositor.service"),
71 ];
72 for unit in &unit_names {
73 let mut cmd = Command::new("systemctl");
74 cmd.args(["--user", "reset-failed", unit])
75 .stdout(std::process::Stdio::null())
76 .stderr(std::process::Stdio::null());
77 let _ = cmd.status();
78 }
79 Ok(())
80}
81
82pub fn enable_now_socket(name: &str) -> Result<()> {
84 if !is_available() {
85 return Ok(());
86 }
87 let mut cmd = Command::new("systemctl");
88 cmd.args(["--user", "enable", "--now", &format!("{name}.socket")]);
89 let status = cmd
90 .status()
91 .context("failed to spawn systemctl enable --now")?;
92 if !status.success() {
93 anyhow::bail!("systemctl --user enable --now {name}.socket failed");
94 }
95 Ok(())
96}
97
98pub fn stop_socket_and_host(name: &str) -> Result<()> {
100 if !is_available() {
101 return Ok(());
102 }
103 for unit in [format!("{name}.socket"), format!("{name}-host.service")] {
104 let mut cmd = Command::new("systemctl");
105 cmd.args(["--user", "stop", &unit]);
106 let _ = cmd.status();
107 }
108 Ok(())
109}
110
111pub fn stop_compositor(name: &str) -> Result<()> {
113 if !is_available() {
114 return Ok(());
115 }
116 let mut cmd = Command::new("systemctl");
117 cmd.args(["--user", "stop", &format!("{name}-compositor.service")]);
118 let _ = cmd.status();
119 Ok(())
120}
121
122pub(crate) fn guest_socket_path(name: &str) -> std::path::PathBuf {
124 let runtime = std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| {
125 let uid = nix::unistd::getuid().as_raw();
126 format!("/run/user/{uid}")
127 });
128 std::path::PathBuf::from(runtime)
129 .join("podbox")
130 .join(format!("{name}.sock"))
131}
132
133fn rebind_guest_socket(name: &str) -> Result<()> {
140 let mut cmd = Command::new("systemctl");
141 cmd.args(["--user", "restart", &format!("{name}.socket")]);
142 let status = cmd.status().context("failed to spawn systemctl restart")?;
143 if !status.success() {
144 anyhow::bail!("systemctl --user restart {name}.socket failed");
145 }
146 Ok(())
147}
148
149pub(crate) fn heal_missing_guest_socket(name: &str) -> Result<bool> {
154 if guest_socket_path(name).exists() {
155 return Ok(false);
156 }
157 eprintln!(
158 "Warning: {} is missing but {}.socket is active — restarting the socket unit to rebind it.",
159 guest_socket_path(name).display(),
160 name
161 );
162 rebind_guest_socket(name)?;
163 Ok(true)
164}
165
166pub fn start_unit(name: &str) -> Result<()> {
168 let mut cmd = Command::new("systemctl");
169 cmd.args(["--user", "start", &format!("{name}.service")]);
170 let status = cmd.status().context("failed to spawn systemctl start")?;
171 if !status.success() {
172 anyhow::bail!("systemctl start failed for '{name}.service'");
173 }
174 Ok(())
175}
176
177pub fn stop_unit(name: &str) -> Result<()> {
179 let mut cmd = Command::new("systemctl");
180 cmd.args(["--user", "stop", &format!("{name}.service")]);
181 cmd.status()?;
182 Ok(())
183}
184
185pub fn restart_unit(name: &str) -> Result<()> {
187 let mut cmd = Command::new("systemctl");
188 cmd.args(["--user", "restart", &format!("{name}.service")]);
189 cmd.status()?;
190 Ok(())
191}
192
193pub fn is_unit_enabled(name: &str) -> bool {
195 if !is_available() {
196 return false;
197 }
198 Command::new("systemctl")
199 .args([
200 "--user",
201 "--quiet",
202 "is-enabled",
203 &format!("{name}.service"),
204 ])
205 .status()
206 .map(|s| s.success())
207 .unwrap_or(false)
208}
209
210pub fn is_unit_failed(name: &str) -> bool {
212 if !is_available() {
213 return false;
214 }
215 Command::new("systemctl")
216 .args(["--user", "is-failed", &format!("{name}.service")])
217 .output()
218 .ok()
219 .map(|o| String::from_utf8_lossy(&o.stdout).trim() == "failed")
220 .unwrap_or(false)
221}