Skip to main content

scxtui/backend/
scxcli.rs

1// SPDX-License-Identifier: GPL-2.0
2
3//! `scxcli` as a child process.
4//!
5//! Reads are `scxcli list --json`, parsed into the types of the published
6//! contract (`model`). Writes are `scxcli start|stop|run` run directly
7//! when scxtui is root, and through `pkexec` otherwise, so scxtui itself
8//! never needs root and never touches the configuration.
9
10use 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
19/// Where the shipped polkit policy expects scxcli. A scxcli found
20/// elsewhere still runs, but polkit then matches no action and pkexec
21/// falls back to asking for an administrator's password for everything.
22const POLICY_PATH: &str = "/usr/bin/scxcli";
23
24pub struct ScxcliBackend {
25    root: bool,
26    /// The absolute path scxcli resolved to, passed as such to pkexec:
27    /// polkit matches the action on the program's path, so the name is
28    /// resolved here, once, in the trusted search path, not by pkexec
29    /// against whatever PATH it has.
30    scxcli: PathBuf,
31}
32
33impl ScxcliBackend {
34    /// Checks that scxcli is installed, so startup fails fast with a
35    /// plain message instead of the first key press.
36    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    /// A warning for startup when scxcli is not where the policy
53    /// expects it, since every privileged action then prompts.
54    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    /// A privileged scxcli command: direct as root, else via pkexec.
65    /// Both are resolved in systemd's default search path, not scxtui's
66    /// PATH.
67    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}