1use std::path::{Path, PathBuf};
11use std::process::{Command, Stdio};
12
13use crate::model::{Listing, OUTPUT_VERSION};
14use crate::system::{system_command, system_executable};
15use anyhow::{Context, Result, bail};
16
17use super::{Backend, Report};
18
19const POLICY_PATH: &str = "/usr/bin/scxcli";
23
24pub struct ScxcliBackend {
25 root: bool,
26 scxcli: PathBuf,
31}
32
33impl ScxcliBackend {
34 pub fn connect() -> Result<Self> {
37 let scxcli = system_executable("scxcli")
38 .context("scxcli is not installed in systemd's default search path")?;
39 let out = Command::new(&scxcli)
40 .arg("--version")
41 .output()
42 .context("cannot run scxcli")?;
43 if !out.status.success() {
44 bail!("scxcli --version failed: {}", stderr(&out));
45 }
46 Ok(Self {
47 root: is_root(),
48 scxcli,
49 })
50 }
51
52 pub fn policy_mismatch(&self) -> Option<String> {
55 (!self.root && self.scxcli != Path::new(POLICY_PATH)).then(|| {
56 format!(
57 "scxcli is {} but the polkit policy names {POLICY_PATH}; every change will ask \
58for a password",
59 self.scxcli.display()
60 )
61 })
62 }
63
64 fn privileged(&self, args: &[&str]) -> Result<Command> {
68 let mut command = if self.root {
69 Command::new(&self.scxcli)
70 } else {
71 let mut pkexec = system_command("pkexec")
72 .context("pkexec is not installed; run scxtui as root to change schedulers")?;
73 pkexec.arg(&self.scxcli);
74 pkexec
75 };
76 command.args(args);
77 Ok(command)
78 }
79
80 fn write(&self, args: &[&str]) -> Result<Report> {
81 let mut command = self.privileged(args)?;
82 command.stdout(Stdio::piped()).stderr(Stdio::piped());
83 let out = command.output().context("cannot run scxcli")?;
84 if !out.status.success() {
85 bail!("scxcli {} failed: {}", args.join(" "), stderr(&out));
86 }
87 Ok(String::from_utf8_lossy(&out.stdout).trim().to_owned())
88 }
89}
90
91fn is_root() -> bool {
92 std::fs::metadata("/proc/self")
93 .map(|m| std::os::unix::fs::MetadataExt::uid(&m) == 0)
94 .unwrap_or(false)
95}
96
97fn stderr(out: &std::process::Output) -> String {
98 let text = String::from_utf8_lossy(&out.stderr).trim().to_owned();
99 if text.is_empty() {
100 format!("exit status {}", out.status)
101 } else {
102 text
103 }
104}
105
106impl Backend for ScxcliBackend {
107 fn label(&self) -> String {
108 if self.root {
109 "scxcli (root)".to_owned()
110 } else {
111 "scxcli via pkexec".to_owned()
112 }
113 }
114
115 fn list(&self) -> Result<Listing> {
116 let out = Command::new(&self.scxcli)
117 .args(["list", "--json"])
118 .output()
119 .context("cannot run scxcli list")?;
120 if !out.status.success() {
121 bail!("scxcli list failed: {}", stderr(&out));
122 }
123 let listing: Listing =
124 serde_json::from_slice(&out.stdout).context("scxcli list printed unexpected JSON")?;
125 if listing.version != OUTPUT_VERSION {
126 bail!(
127 "scxcli prints output version {}, this scxtui reads {}",
128 listing.version,
129 OUTPUT_VERSION
130 );
131 }
132 Ok(listing)
133 }
134
135 fn start(&self, id: &str) -> Result<Report> {
136 self.write(&["start", id])
137 }
138
139 fn run(&self, sched: &str, args: &[String]) -> Result<Report> {
140 let mut argv = vec!["run", sched];
141 argv.extend(args.iter().map(String::as_str));
142 self.write(&argv)
143 }
144
145 fn stop(&self) -> Result<Report> {
146 self.write(&["stop"])
147 }
148
149 fn escalates(&self) -> bool {
150 !self.root
151 }
152}