planarian_backend_linux/
runtime.rs1use crate::admitted;
7use crate::cmd::run;
8use planarian_schema::{OpId, Outcome, RuntimeOptionBackend, Value};
9use std::path::PathBuf;
10use std::time::Duration;
11
12#[derive(Clone, Debug)]
13pub struct Runtime {
14 txn_dir: PathBuf,
15 pub wifi_permitted: Vec<String>,
18}
19
20impl Runtime {
21 pub fn new(txn_dir: PathBuf) -> Runtime {
22 Runtime { txn_dir, wifi_permitted: vec![] }
23 }
24}
25
26pub fn active_wifi(text: &str) -> Option<String> {
28 text.lines().find_map(|l| {
29 let (name, kind) = l.rsplit_once(':')?;
30 (kind == "802-11-wireless").then(|| name.replace("\\:", ":"))
31 })
32}
33
34impl RuntimeOptionBackend for Runtime {
35 fn get(&self, path: &str) -> Option<Value> {
36 match path {
37 "wifi.network" => {
38 let o =
39 run(&["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show", "--active"], Duration::from_secs(5));
40 active_wifi(&o.stdout).map(Value::Str)
41 }
42 _ => None,
43 }
44 }
45
46 fn set(&self, path: &str, value: &Value, op: &OpId) -> Outcome {
47 if !admitted(&self.txn_dir, op) {
48 return Outcome::fenced();
49 }
50 match (path, value) {
51 ("wifi.network", Value::Str(ssid)) => {
52 if !self.wifi_permitted.is_empty() && !self.wifi_permitted.contains(ssid) {
56 return Outcome::Failed(format!("{ssid} is not a permitted Wi-Fi network"));
57 }
58 let saved = run(&["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"], Duration::from_secs(10));
59 if !wifi_connections(&saved.stdout).contains(ssid) {
60 return Outcome::Failed(format!("{ssid} is not a saved Wi-Fi connection"));
61 }
62 let o = run(&["nmcli", "connection", "up", "id", ssid], Duration::from_secs(60));
63 if o.ok() { Outcome::Succeeded } else { Outcome::Failed(o.tail()) }
64 }
65 _ => Outcome::Failed(format!("{path} has no live mechanism; it applies through the configuration")),
66 }
67 }
68
69 fn alternatives(&self, path: &str) -> Vec<Value> {
70 if path != "wifi.network" {
71 return vec![];
72 }
73 let t = Duration::from_secs(20);
74 let saved = run(&["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"], t);
75 let active = run(&["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show", "--active"], t);
76 let scan = run(&["nmcli", "-t", "-f", "SSID", "device", "wifi", "list", "--rescan", "yes"], t);
77 let in_range: Vec<String> = scan.stdout.lines().map(|l| l.replace("\\:", ":")).collect();
78 let current = active_wifi(&active.stdout);
79 wifi_connections(&saved.stdout)
80 .into_iter()
81 .filter(|name| Some(name) != current.as_ref())
82 .filter(|name| {
83 let ssid = run(&["nmcli", "-g", "802-11-wireless.ssid", "connection", "show", name], t);
85 in_range.iter().any(|s| s == ssid.stdout.trim())
86 })
87 .map(Value::Str)
88 .collect()
89 }
90}
91
92pub fn wifi_connections(text: &str) -> Vec<String> {
94 text.lines()
95 .filter_map(|l| {
96 let (name, kind) = l.rsplit_once(':')?;
97 (kind == "802-11-wireless").then(|| name.replace("\\:", ":"))
98 })
99 .collect()
100}
101
102#[cfg(test)]
103mod tests {
104 #[test]
105 fn wifi() {
106 assert_eq!(super::active_wifi("Wired:802-3-ethernet\nhome\\:5G:802-11-wireless\n"), Some("home:5G".into()));
107 assert_eq!(super::active_wifi(""), None);
108 assert_eq!(
109 super::wifi_connections("Wired:802-3-ethernet\ngym-ap-a:802-11-wireless\ngym-ap-b:802-11-wireless\n"),
110 vec!["gym-ap-a".to_string(), "gym-ap-b".to_string()]
111 );
112 }
113}