1use crate::proto::{respond, send_msg, Out};
9use crate::store;
10use crate::{bus, exec, fsops, hooks, 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 stryke_lsp: Option<crate::stryke_lsp::StrykeLsp>,
54 #[cfg(feature = "sysinfo-caps")]
55 sysmon: Option<crate::sysmon::Monitor>,
56 sub_id: Option<u64>,
58 peer_id: Option<u64>,
60 authed: bool,
63}
64
65impl Drop for Session {
66 fn drop(&mut self) {
67 if let Some(id) = self.sub_id {
68 bus::unregister(id);
69 }
70 if let Some(id) = self.peer_id {
71 peer::unregister_link(id);
72 }
73 }
74}
75
76impl Default for Session {
77 fn default() -> Self {
78 Self::new()
79 }
80}
81
82impl Session {
83 pub fn new() -> Self {
85 Self::guarded(false)
86 }
87
88 pub fn guarded(require_auth: bool) -> Self {
91 Session {
92 #[cfg(feature = "pty")]
93 ptys: HashMap::new(),
94 watchers: HashMap::new(),
95 stryke_lsp: None,
96 #[cfg(feature = "sysinfo-caps")]
97 sysmon: None,
98 sub_id: None,
99 peer_id: None,
100 authed: !require_auth,
101 }
102 }
103
104 fn key(req: &Value) -> String {
107 req["id"].as_str().unwrap_or("").to_string()
108 }
109
110 fn app(req: &Value) -> String {
112 req["app"].as_str().unwrap_or("zwire").to_string()
113 }
114
115 pub fn handle(&mut self, out: &Out, msg: &Value) {
118 crate::hostlog::record("tx", msg, msg);
121 if let Some(cmd) = msg["cmd"].as_str() {
122 self.handle_cmd(out, msg, cmd);
123 return;
124 }
125 if !msg["ui"].is_null() {
127 let ui = store::write_ui(&store::theme_dir(), &msg["ui"]);
128 crate::theme_watch::note_ui(&ui); bus::publish("ui", &ui);
132 peer::broadcast("ui", &ui);
133 respond(out, msg, json!({"ok": true, "ui": ui}));
134 } else if let Some(s) = msg["scheme"].as_str() {
135 self.set_scheme(out, msg, s);
136 } else {
137 respond(out, msg, json!({"ok": false, "err": "empty"}));
138 }
139 }
140
141 fn handle_cmd(&mut self, out: &Out, msg: &Value, cmd: &str) {
142 if !self.authed && !matches!(cmd, "auth" | "peer_hello" | "hello" | "ping") {
145 respond(out, msg, json!({"ok": false, "err": "unauthorized"}));
146 return;
147 }
148 match cmd {
149 "hello" | "ping" => respond(
150 out,
151 msg,
152 json!({
153 "ok": true, "host": "zwire-host", "version": crate::VERSION,
154 "os": std::env::consts::OS, "arch": std::env::consts::ARCH,
155 "pid": std::process::id(), "caps": caps(),
156 }),
157 ),
158
159 "kv_get" => {
161 let v = store::kv_get(&Self::app(msg), msg["key"].as_str().unwrap_or(""));
162 respond(out, msg, json!({"ok": true, "value": v}));
163 }
164 "kv_set" => {
165 let ok = store::kv_set(
166 &Self::app(msg),
167 msg["key"].as_str().unwrap_or(""),
168 &msg["value"],
169 );
170 respond(out, msg, json!({ "ok": ok }));
171 }
172 "kv_merge" => {
173 let v = store::kv_merge(
174 &Self::app(msg),
175 msg["key"].as_str().unwrap_or(""),
176 &msg["value"],
177 );
178 respond(out, msg, json!({"ok": true, "value": v}));
179 }
180 "kv_del" => {
181 let ok = store::kv_del(&Self::app(msg), msg["key"].as_str().unwrap_or(""));
182 respond(out, msg, json!({ "ok": ok }));
183 }
184 "kv_keys" => {
185 let keys = store::kv_keys(&Self::app(msg));
186 respond(out, msg, json!({"ok": true, "keys": keys}));
187 }
188
189 "get" => {
191 let d = store::theme_dir();
192 respond(
193 out,
194 msg,
195 json!({"ok": true, "version": crate::VERSION, "scheme": store::current_scheme(&d), "ui": store::current_ui(&d)}),
196 );
197 }
198
199 "hostlog" => {
201 let n = msg["limit"].as_u64().unwrap_or(300) as usize;
202 respond(
203 out,
204 msg,
205 json!({"ok": true, "log": crate::hostlog::read_tail(n)}),
206 );
207 }
208
209 "hooks_list" => respond(out, msg, hooks::list()),
211 "hooks_events" => respond(out, msg, hooks::events()),
212 "hooks_save" => respond(out, msg, hooks::save(msg)),
213 "hooks_delete" => respond(out, msg, hooks::delete(msg)),
214 "hooks_set_enabled" => respond(out, msg, hooks::set_enabled(msg)),
215 "hooks_get_script" => respond(out, msg, hooks::get_script(msg)),
216 "hooks_script_path" => respond(out, msg, hooks::script_path_of(msg)),
217 "hooks_set_script" => respond(out, msg, hooks::set_script(msg)),
218 "hooks_test_run" => respond(out, msg, hooks::test_run(msg)),
219 "hook_fire" => respond(out, msg, hooks::fire_cmd(msg)),
220
221 "stryke_lsp_start" => {
224 self.stryke_lsp = None; match crate::stryke_lsp::StrykeLsp::start(out) {
226 Ok(l) => {
227 self.stryke_lsp = Some(l);
228 respond(out, msg, json!({"ok": true}));
229 }
230 Err(e) => respond(out, msg, json!({"ok": false, "err": e})),
231 }
232 }
233 "stryke_lsp_send" => {
234 let m = msg["message"].as_str().unwrap_or("");
235 match self.stryke_lsp.as_mut() {
236 Some(l) => match l.send(m) {
237 Ok(()) => respond(out, msg, json!({"ok": true})),
238 Err(e) => respond(out, msg, json!({"ok": false, "err": e})),
239 },
240 None => respond(
241 out,
242 msg,
243 json!({"ok": false, "err": "stryke language server not running"}),
244 ),
245 }
246 }
247 "stryke_lsp_stop" => {
248 self.stryke_lsp = None;
249 respond(out, msg, json!({"ok": true}));
250 }
251 "stryke_run" => {
254 let code = msg["code"].as_str().unwrap_or("");
255 let stdin = msg["stdin"].as_str().unwrap_or("");
256 match crate::stryke_runner::run_code(
257 code,
258 stdin,
259 std::time::Duration::from_secs(10),
260 ) {
261 Ok(o) => respond(
262 out,
263 msg,
264 json!({"ok": true, "stdout": o.stdout, "stderr": o.stderr,
265 "code": o.code, "timedOut": o.timed_out}),
266 ),
267 Err(e) => respond(out, msg, json!({"ok": false, "err": e})),
268 }
269 }
270
271 "fs_watch" | "fs_tail" => {
273 let key = Self::key(msg);
274 let watcher = if cmd == "fs_watch" {
275 watch::Watcher::fs_watch(out, msg, key.clone())
276 } else {
277 watch::Watcher::fs_tail(out, msg, key.clone())
278 };
279 self.watchers.insert(key, watcher);
280 respond(out, msg, json!({"ok": true, "watching": true}));
281 }
282 "meter_stream" => {
284 let key = Self::key(msg);
285 let watcher = watch::Watcher::meter_stream(out, msg, key.clone());
286 self.watchers.insert(key, watcher);
287 respond(out, msg, json!({"ok": true, "streaming": true}));
288 }
289 "watch_stop" => {
290 self.watchers.remove(&Self::key(msg));
291 respond(out, msg, json!({"ok": true}));
292 }
293 "watch_list" => {
294 let mut ids: Vec<&String> = self.watchers.keys().collect();
295 ids.sort();
296 respond(out, msg, json!({"ok": true, "watchers": ids}));
297 }
298
299 c if c.starts_with("fs_") => {
301 let reply = fsops::handle(c, msg);
302 respond(out, msg, reply);
303 }
304
305 "exec" => respond(out, msg, exec::run(msg)),
307
308 c if c.starts_with("job_") => {
310 let reply = jobs::handle(c, msg);
311 respond(out, msg, reply);
312 }
313
314 #[cfg(feature = "sysinfo-caps")]
316 "ps" | "kill" | "which" => respond(out, msg, crate::procs::handle(cmd, msg)),
317
318 "sub" => {
320 let id = *self.sub_id.get_or_insert_with(|| bus::register(out));
321 if let Some(topic) = msg["topic"].as_str() {
322 bus::subscribe(id, topic);
323 respond(out, msg, json!({"ok": true, "topic": topic}));
324 if topic == "ui" || topic == "scheme" {
327 crate::theme_watch::ensure_started();
328 }
329 let d = store::theme_dir();
336 match topic {
337 "scheme" => bus::send_one(
338 out,
339 "scheme",
340 &json!({ "scheme": store::current_scheme(&d) }),
341 ),
342 "ui" => bus::send_one(out, "ui", &store::current_ui(&d)),
343 _ => {}
344 }
345 } else {
346 respond(out, msg, json!({"ok": false, "err": "no_topic"}));
347 }
348 }
349 "unsub" => {
350 if let (Some(id), Some(topic)) = (self.sub_id, msg["topic"].as_str()) {
351 bus::unsubscribe(id, topic);
352 }
353 respond(out, msg, json!({"ok": true}));
354 }
355 "pub" => match msg["topic"].as_str() {
356 Some(topic) => {
357 let delivered = bus::publish(topic, &msg["data"]);
358 let forwarded = peer::broadcast(topic, &msg["data"]);
359 respond(
360 out,
361 msg,
362 json!({"ok": true, "delivered": delivered, "forwarded": forwarded}),
363 );
364 }
365 None => respond(out, msg, json!({"ok": false, "err": "no_topic"})),
366 },
367
368 "auth" => {
370 if peer::token_ok(msg["token"].as_str()) {
371 self.authed = true;
372 respond(out, msg, json!({"ok": true}));
373 } else {
374 respond(out, msg, json!({"ok": false, "err": "unauthorized"}));
375 }
376 }
377 "peer_hello" => {
378 if peer::token_ok(msg["token"].as_str()) {
379 self.authed = true;
380 let name = msg["name"].as_str().unwrap_or("peer");
381 self.peer_id = Some(peer::register_link(out, name));
382 respond(out, msg, json!({"ok": true, "name": peer::local_name()}));
383 } else {
384 respond(out, msg, json!({"ok": false, "err": "unauthorized"}));
385 }
386 }
387 "peer_pub" => {
390 if let Some(topic) = msg["topic"].as_str() {
391 bus::publish(topic, &msg["data"]);
392 }
393 }
394 "peers" => respond(
395 out,
396 msg,
397 json!({"ok": true, "self": peer::local_name(), "peers": peer::peer_names()}),
398 ),
399 "peer_connect" => match msg["addr"].as_str() {
400 Some(addr) => {
401 peer::dial(addr.to_string());
402 respond(out, msg, json!({"ok": true, "dialing": addr}));
403 }
404 None => respond(out, msg, json!({"ok": false, "err": "no_addr"})),
405 },
406 "remote" => match msg["peer"].as_str() {
407 Some(addr) => match peer::remote(addr, &msg["request"]) {
408 Ok(reply) => {
409 respond(out, msg, json!({"ok": true, "peer": addr, "reply": reply}))
410 }
411 Err(e) => respond(out, msg, json!({"ok": false, "err": e})),
412 },
413 None => respond(out, msg, json!({"ok": false, "err": "no_peer"})),
414 },
415
416 "open" | "clipboard_get" | "clipboard_set" | "notify" | "hostinfo" => {
418 respond(out, msg, osops::handle(cmd, msg));
419 }
420
421 #[cfg(feature = "sysinfo-caps")]
423 "sysinfo_start" => {
424 let interval = msg["interval_ms"].as_u64().unwrap_or(2000);
425 self.sysmon = Some(crate::sysmon::Monitor::start(out, interval, Self::key(msg)));
426 respond(out, msg, json!({"ok": true, "streaming": true}));
427 }
428 #[cfg(feature = "sysinfo-caps")]
429 "sysinfo_stop" => {
430 self.sysmon = None;
431 respond(out, msg, json!({"ok": true, "streaming": false}));
432 }
433 #[cfg(feature = "sysinfo-caps")]
434 "sysinfo_once" => {
435 use sysinfo::{Disks, Networks, System};
436 let mut sys = System::new();
437 sys.refresh_cpu_usage();
438 sys.refresh_memory();
439 let nets = Networks::new_with_refreshed_list();
440 let mut disks = Disks::new_with_refreshed_list();
445 disks.refresh(true);
446 respond(
447 out,
448 msg,
449 json!({"ok": true, "sys": crate::sysmon::snapshot(1.0, &nets, &disks, &sys)}),
450 );
451 }
452
453 #[cfg(feature = "pty")]
455 "pty_spawn" => {
456 let key = Self::key(msg);
457 match crate::pty::PtySession::spawn(out, msg, key.clone()) {
458 Some(s) => {
459 self.ptys.insert(key, s);
460 respond(out, msg, json!({"ok": true, "spawned": true}));
461 }
462 None => respond(out, msg, json!({"ok": false, "err": "pty_spawn_failed"})),
463 }
464 }
465 #[cfg(feature = "pty")]
466 "pty_write" => {
467 if let Some(s) = self.ptys.get_mut(&Self::key(msg)) {
468 s.write(msg);
469 }
470 }
471 #[cfg(feature = "pty")]
472 "pty_resize" => {
473 if let Some(s) = self.ptys.get(&Self::key(msg)) {
474 s.resize(msg);
475 }
476 }
477 #[cfg(feature = "pty")]
478 "pty_kill" => {
479 self.ptys.remove(&Self::key(msg));
482 }
483
484 _ => {
485 let _ = send_msg(out, &json!({"ok": false, "err": "unknown_cmd", "cmd": cmd}));
486 }
487 }
488 }
489
490 fn set_scheme(&self, out: &Out, msg: &Value, s: &str) {
491 if store::SCHEMES.contains(&s) {
492 store::write_scheme(&store::theme_dir(), s);
493 crate::theme_watch::note_scheme(s); let data = json!({ "scheme": s });
497 bus::publish("scheme", &data);
498 peer::broadcast("scheme", &data);
499 respond(out, msg, json!({"ok": true, "scheme": s}));
500 } else {
501 respond(out, msg, json!({"ok": false, "err": "bad_scheme"}));
502 }
503 }
504}