1use crate::proto::{respond, send_msg, Out};
9use crate::store;
10use crate::{bus, exec, fsops, jobs, osops, peer, watch};
11use serde_json::{json, Value};
12use std::collections::HashMap;
13
14pub fn caps() -> Vec<&'static str> {
17 #[cfg_attr(not(any(feature = "sysinfo-caps", feature = "pty")), allow(unused_mut))]
18 let mut c = vec![
19 "hello",
20 "kv",
21 "fs",
22 "exec",
23 "jobs",
24 "bus",
25 "peer",
26 "watch",
27 "open",
28 "clipboard",
29 "notify",
30 "hostinfo",
31 "scheme",
32 "ui",
33 ];
34 #[cfg(feature = "sysinfo-caps")]
35 {
36 c.push("sysinfo");
37 c.push("procs");
38 }
39 #[cfg(feature = "pty")]
40 c.push("pty");
41 c
42}
43
44pub struct Session {
48 #[cfg(feature = "pty")]
49 ptys: HashMap<String, crate::pty::PtySession>,
50 watchers: HashMap<String, watch::Watcher>,
51 #[cfg(feature = "sysinfo-caps")]
52 sysmon: Option<crate::sysmon::Monitor>,
53 sub_id: Option<u64>,
55 peer_id: Option<u64>,
57 authed: bool,
60}
61
62impl Drop for Session {
63 fn drop(&mut self) {
64 if let Some(id) = self.sub_id {
65 bus::unregister(id);
66 }
67 if let Some(id) = self.peer_id {
68 peer::unregister_link(id);
69 }
70 }
71}
72
73impl Default for Session {
74 fn default() -> Self {
75 Self::new()
76 }
77}
78
79impl Session {
80 pub fn new() -> Self {
82 Self::guarded(false)
83 }
84
85 pub fn guarded(require_auth: bool) -> Self {
88 Session {
89 #[cfg(feature = "pty")]
90 ptys: HashMap::new(),
91 watchers: HashMap::new(),
92 #[cfg(feature = "sysinfo-caps")]
93 sysmon: None,
94 sub_id: None,
95 peer_id: None,
96 authed: !require_auth,
97 }
98 }
99
100 fn key(req: &Value) -> String {
103 req["id"].as_str().unwrap_or("").to_string()
104 }
105
106 fn app(req: &Value) -> String {
108 req["app"].as_str().unwrap_or("zwire").to_string()
109 }
110
111 pub fn handle(&mut self, out: &Out, msg: &Value) {
114 crate::hostlog::record("tx", msg, msg);
117 if let Some(cmd) = msg["cmd"].as_str() {
118 self.handle_cmd(out, msg, cmd);
119 return;
120 }
121 if !msg["ui"].is_null() {
123 let ui = store::write_ui(&store::theme_dir(), &msg["ui"]);
124 crate::theme_watch::note_ui(&ui); bus::publish("ui", &ui);
128 peer::broadcast("ui", &ui);
129 respond(out, msg, json!({"ok": true, "ui": ui}));
130 } else if let Some(s) = msg["scheme"].as_str() {
131 self.set_scheme(out, msg, s);
132 } else {
133 respond(out, msg, json!({"ok": false, "err": "empty"}));
134 }
135 }
136
137 fn handle_cmd(&mut self, out: &Out, msg: &Value, cmd: &str) {
138 if !self.authed && !matches!(cmd, "auth" | "peer_hello" | "hello" | "ping") {
141 respond(out, msg, json!({"ok": false, "err": "unauthorized"}));
142 return;
143 }
144 match cmd {
145 "hello" | "ping" => respond(
146 out,
147 msg,
148 json!({
149 "ok": true, "host": "zwire-host", "version": crate::VERSION,
150 "os": std::env::consts::OS, "arch": std::env::consts::ARCH,
151 "pid": std::process::id(), "caps": caps(),
152 }),
153 ),
154
155 "kv_get" => {
157 let v = store::kv_get(&Self::app(msg), msg["key"].as_str().unwrap_or(""));
158 respond(out, msg, json!({"ok": true, "value": v}));
159 }
160 "kv_set" => {
161 let ok = store::kv_set(
162 &Self::app(msg),
163 msg["key"].as_str().unwrap_or(""),
164 &msg["value"],
165 );
166 respond(out, msg, json!({ "ok": ok }));
167 }
168 "kv_merge" => {
169 let v = store::kv_merge(
170 &Self::app(msg),
171 msg["key"].as_str().unwrap_or(""),
172 &msg["value"],
173 );
174 respond(out, msg, json!({"ok": true, "value": v}));
175 }
176 "kv_del" => {
177 let ok = store::kv_del(&Self::app(msg), msg["key"].as_str().unwrap_or(""));
178 respond(out, msg, json!({ "ok": ok }));
179 }
180 "kv_keys" => {
181 let keys = store::kv_keys(&Self::app(msg));
182 respond(out, msg, json!({"ok": true, "keys": keys}));
183 }
184
185 "get" => {
187 let d = store::theme_dir();
188 respond(
189 out,
190 msg,
191 json!({"ok": true, "scheme": store::current_scheme(&d), "ui": store::current_ui(&d)}),
192 );
193 }
194
195 "hostlog" => {
197 let n = msg["limit"].as_u64().unwrap_or(300) as usize;
198 respond(
199 out,
200 msg,
201 json!({"ok": true, "log": crate::hostlog::read_tail(n)}),
202 );
203 }
204
205 "fs_watch" | "fs_tail" => {
207 let key = Self::key(msg);
208 let watcher = if cmd == "fs_watch" {
209 watch::Watcher::fs_watch(out, msg, key.clone())
210 } else {
211 watch::Watcher::fs_tail(out, msg, key.clone())
212 };
213 self.watchers.insert(key, watcher);
214 respond(out, msg, json!({"ok": true, "watching": true}));
215 }
216 "watch_stop" => {
217 self.watchers.remove(&Self::key(msg));
218 respond(out, msg, json!({"ok": true}));
219 }
220 "watch_list" => {
221 let mut ids: Vec<&String> = self.watchers.keys().collect();
222 ids.sort();
223 respond(out, msg, json!({"ok": true, "watchers": ids}));
224 }
225
226 c if c.starts_with("fs_") => {
228 let reply = fsops::handle(c, msg);
229 respond(out, msg, reply);
230 }
231
232 "exec" => respond(out, msg, exec::run(msg)),
234
235 c if c.starts_with("job_") => {
237 let reply = jobs::handle(c, msg);
238 respond(out, msg, reply);
239 }
240
241 #[cfg(feature = "sysinfo-caps")]
243 "ps" | "kill" | "which" => respond(out, msg, crate::procs::handle(cmd, msg)),
244
245 "sub" => {
247 let id = *self.sub_id.get_or_insert_with(|| bus::register(out));
248 if let Some(topic) = msg["topic"].as_str() {
249 bus::subscribe(id, topic);
250 respond(out, msg, json!({"ok": true, "topic": topic}));
251 if topic == "ui" || topic == "scheme" {
254 crate::theme_watch::ensure_started();
255 }
256 let d = store::theme_dir();
263 match topic {
264 "scheme" => bus::send_one(
265 out,
266 "scheme",
267 &json!({ "scheme": store::current_scheme(&d) }),
268 ),
269 "ui" => bus::send_one(out, "ui", &store::current_ui(&d)),
270 _ => {}
271 }
272 } else {
273 respond(out, msg, json!({"ok": false, "err": "no_topic"}));
274 }
275 }
276 "unsub" => {
277 if let (Some(id), Some(topic)) = (self.sub_id, msg["topic"].as_str()) {
278 bus::unsubscribe(id, topic);
279 }
280 respond(out, msg, json!({"ok": true}));
281 }
282 "pub" => match msg["topic"].as_str() {
283 Some(topic) => {
284 let delivered = bus::publish(topic, &msg["data"]);
285 let forwarded = peer::broadcast(topic, &msg["data"]);
286 respond(
287 out,
288 msg,
289 json!({"ok": true, "delivered": delivered, "forwarded": forwarded}),
290 );
291 }
292 None => respond(out, msg, json!({"ok": false, "err": "no_topic"})),
293 },
294
295 "auth" => {
297 if peer::token_ok(msg["token"].as_str()) {
298 self.authed = true;
299 respond(out, msg, json!({"ok": true}));
300 } else {
301 respond(out, msg, json!({"ok": false, "err": "unauthorized"}));
302 }
303 }
304 "peer_hello" => {
305 if peer::token_ok(msg["token"].as_str()) {
306 self.authed = true;
307 let name = msg["name"].as_str().unwrap_or("peer");
308 self.peer_id = Some(peer::register_link(out, name));
309 respond(out, msg, json!({"ok": true, "name": peer::local_name()}));
310 } else {
311 respond(out, msg, json!({"ok": false, "err": "unauthorized"}));
312 }
313 }
314 "peer_pub" => {
317 if let Some(topic) = msg["topic"].as_str() {
318 bus::publish(topic, &msg["data"]);
319 }
320 }
321 "peers" => respond(
322 out,
323 msg,
324 json!({"ok": true, "self": peer::local_name(), "peers": peer::peer_names()}),
325 ),
326 "peer_connect" => match msg["addr"].as_str() {
327 Some(addr) => {
328 peer::dial(addr.to_string());
329 respond(out, msg, json!({"ok": true, "dialing": addr}));
330 }
331 None => respond(out, msg, json!({"ok": false, "err": "no_addr"})),
332 },
333 "remote" => match msg["peer"].as_str() {
334 Some(addr) => match peer::remote(addr, &msg["request"]) {
335 Ok(reply) => {
336 respond(out, msg, json!({"ok": true, "peer": addr, "reply": reply}))
337 }
338 Err(e) => respond(out, msg, json!({"ok": false, "err": e})),
339 },
340 None => respond(out, msg, json!({"ok": false, "err": "no_peer"})),
341 },
342
343 "open" | "clipboard_get" | "clipboard_set" | "notify" | "hostinfo" => {
345 respond(out, msg, osops::handle(cmd, msg));
346 }
347
348 #[cfg(feature = "sysinfo-caps")]
350 "sysinfo_start" => {
351 let interval = msg["interval_ms"].as_u64().unwrap_or(2000);
352 self.sysmon = Some(crate::sysmon::Monitor::start(out, interval, Self::key(msg)));
353 respond(out, msg, json!({"ok": true, "streaming": true}));
354 }
355 #[cfg(feature = "sysinfo-caps")]
356 "sysinfo_stop" => {
357 self.sysmon = None;
358 respond(out, msg, json!({"ok": true, "streaming": false}));
359 }
360 #[cfg(feature = "sysinfo-caps")]
361 "sysinfo_once" => {
362 use sysinfo::{Disks, Networks, System};
363 let mut sys = System::new();
364 sys.refresh_cpu_usage();
365 sys.refresh_memory();
366 let nets = Networks::new_with_refreshed_list();
367 let mut disks = Disks::new_with_refreshed_list();
372 disks.refresh(true);
373 respond(
374 out,
375 msg,
376 json!({"ok": true, "sys": crate::sysmon::snapshot(1.0, &nets, &disks, &sys)}),
377 );
378 }
379
380 #[cfg(feature = "pty")]
382 "pty_spawn" => {
383 let key = Self::key(msg);
384 match crate::pty::PtySession::spawn(out, msg, key.clone()) {
385 Some(s) => {
386 self.ptys.insert(key, s);
387 respond(out, msg, json!({"ok": true, "spawned": true}));
388 }
389 None => respond(out, msg, json!({"ok": false, "err": "pty_spawn_failed"})),
390 }
391 }
392 #[cfg(feature = "pty")]
393 "pty_write" => {
394 if let Some(s) = self.ptys.get_mut(&Self::key(msg)) {
395 s.write(msg);
396 }
397 }
398 #[cfg(feature = "pty")]
399 "pty_resize" => {
400 if let Some(s) = self.ptys.get(&Self::key(msg)) {
401 s.resize(msg);
402 }
403 }
404 #[cfg(feature = "pty")]
405 "pty_kill" => {
406 self.ptys.remove(&Self::key(msg));
409 }
410
411 _ => {
412 let _ = send_msg(out, &json!({"ok": false, "err": "unknown_cmd", "cmd": cmd}));
413 }
414 }
415 }
416
417 fn set_scheme(&self, out: &Out, msg: &Value, s: &str) {
418 if store::SCHEMES.contains(&s) {
419 store::write_scheme(&store::theme_dir(), s);
420 crate::theme_watch::note_scheme(s); let data = json!({ "scheme": s });
424 bus::publish("scheme", &data);
425 peer::broadcast("scheme", &data);
426 respond(out, msg, json!({"ok": true, "scheme": s}));
427 } else {
428 respond(out, msg, json!({"ok": false, "err": "bad_scheme"}));
429 }
430 }
431}