Skip to main content

scxtui/
model.rs

1// SPDX-License-Identifier: GPL-2.0
2
3//! scxcli's public JSON, as scxtui reads it: the `list --json` output
4//! (version 2). Defined here, against `services/CATALOG.md`, rather
5//! than borrowed from the `scx_catalog` crate: scxtui is a client of
6//! scxcli like any other and depends on the published contract, not on
7//! the crate's API. The fixture test at the bottom reads the shared
8//! fixtures to keep the two in step.
9//!
10//! The option validator mirrors the contract so that the TUI can refuse
11//! a bad option before spawning anything; scxcli's own validation stays
12//! the authoritative one.
13
14use std::path::PathBuf;
15
16use anyhow::{Result, bail};
17use serde::{Deserialize, Serialize};
18
19/// `list --json` and `get --json` version this scxtui reads.
20pub const OUTPUT_VERSION: u32 = 2;
21pub const DEFAULT_ID: &str = "default";
22pub const ADMIN_PATH: &str = "/etc/scx/catalog.json";
23pub const SHIPPED_PATH: &str = "/usr/share/scx/catalog.json";
24
25/// One recommended entry as `list --json` prints it.
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct Pick {
28    pub id: String,
29    pub label: String,
30    #[serde(default)]
31    pub description: String,
32    #[serde(default)]
33    pub path: Option<String>,
34    pub available: bool,
35}
36
37/// One installed scheduler the catalog does not hide.
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39pub struct Installed {
40    pub name: String,
41    pub path: String,
42    pub recommended: bool,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub struct Unit {
47    pub active: bool,
48    pub enabled: bool,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct Running {
53    pub sched: String,
54    pub args: Vec<String>,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58pub struct Next {
59    pub sched: String,
60    pub args: Vec<String>,
61    #[serde(rename = "override")]
62    pub overridden: bool,
63}
64
65impl Next {
66    /// The scheduler by name; the file may hold a path.
67    pub fn name(&self) -> &str {
68        self.sched.rsplit('/').next().unwrap_or(&self.sched)
69    }
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73pub struct Kernel {
74    pub state: String,
75    pub ops: Option<String>,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79pub struct Failure {
80    pub message: String,
81    pub status: String,
82}
83
84/// `get --json`.
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86pub struct Current {
87    pub version: u32,
88    /// The running scheduler by name, `default` while the kernel's own
89    /// runs, null while the unit and the kernel disagree.
90    pub scheduler: Option<String>,
91    pub unit: Unit,
92    pub running: Option<Running>,
93    pub next: Option<Next>,
94    pub kernel: Option<Kernel>,
95    pub failure: Option<Failure>,
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    pub config_error: Option<String>,
98}
99
100impl Current {
101    pub fn attached(&self) -> bool {
102        self.kernel.as_ref().is_some_and(|k| k.ops.is_some())
103    }
104}
105
106/// `list --json`.
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
108pub struct Listing {
109    pub version: u32,
110    pub source: PathBuf,
111    pub customized: bool,
112    pub recommended: Vec<Pick>,
113    #[serde(default)]
114    pub all: Vec<Installed>,
115    #[serde(default)]
116    pub config: Option<PathBuf>,
117    pub current: Current,
118}
119
120/// 1 to 256 characters of ASCII letters, digits and `-_=.,:/+`.
121pub fn validate_arg(arg: &str) -> Result<()> {
122    let ok = (1..=256).contains(&arg.len())
123        && arg
124            .chars()
125            .all(|c| c.is_ascii_alphanumeric() || "-_=.,:/+".contains(c));
126    if !ok {
127        bail!("invalid option {arg:?}: ASCII letters, digits and -_=.,:/+ only");
128    }
129    Ok(())
130}
131
132#[cfg(test)]
133mod tests {
134    use std::fs;
135    use std::path::Path;
136
137    use super::*;
138
139    fn fixtures() -> PathBuf {
140        Path::new(env!("CARGO_MANIFEST_DIR")).join("../../rust/scx_catalog/fixtures")
141    }
142
143    /// The shared fixtures are the contract. Every expected `get` output
144    /// must parse into these types and serialise back to the same JSON,
145    /// so scxtui reads what scxcli prints.
146    #[test]
147    fn the_shared_fixtures_round_trip_through_these_types() {
148        let mut seen = 0;
149        for entry in fs::read_dir(fixtures().join("detection")).unwrap() {
150            let path = entry.unwrap().path();
151            if path.extension().is_none_or(|e| e != "json") {
152                continue;
153            }
154            let text = fs::read_to_string(&path).unwrap();
155            let case: serde_json::Value = serde_json::from_str(&text).unwrap();
156            let value = case["expected"].clone();
157            let current: Current = serde_json::from_value(value.clone())
158                .unwrap_or_else(|err| panic!("{}: {err}", path.display()));
159            assert_eq!(current.version, OUTPUT_VERSION);
160            assert_eq!(
161                serde_json::to_value(&current).unwrap(),
162                value,
163                "{}",
164                path.display()
165            );
166            seen += 1;
167        }
168        assert!(seen >= 10);
169    }
170
171    #[test]
172    fn the_validator_mirrors_the_contract() {
173        validate_arg("--pinned-slice-us=500").unwrap();
174        for bad in ["", "-s 5", "$HOME", "'x'", "a;b"] {
175            assert!(validate_arg(bad).is_err(), "{bad:?}");
176        }
177        assert_eq!(
178            Next {
179                sched: "/usr/bin/scx_lavd".into(),
180                args: Vec::new(),
181                overridden: false
182            }
183            .name(),
184            "scx_lavd"
185        );
186    }
187}