1use std::collections::BTreeMap;
7use std::io::{BufRead, BufReader, Write};
8use std::sync::{Arc, Mutex};
9
10use crate::addon::{
11 Addon, AddonAction, AddonChoices, AddonError, AddonParam, AddonSetting, AddonSignal,
12 Availability, CredentialHandle, Credentials, DeviceAction, DeviceKeystroke, Invocation,
13 Permission, Reading, Store, StoreHandle,
14};
15use crate::protocol::{
16 ActionDecl, Answer, Ask, AvailabilityDecl, ChoiceDecl, ChoicesDecl, Description,
17 DeviceActionDecl, FailureKind, KeystrokeDecl, PROTOCOL, ParamDecl, PermissionDecl, ReadingDecl,
18 Reply, Request, SettingDecl, SignalDecl,
19};
20
21pub fn run<A: Addon>(addon: A) {
26 let stdin = BufReader::new(std::io::stdin());
27 let stdout = std::io::stdout();
28 let _ = serve(addon, stdin, stdout);
31}
32
33pub fn serve<A: Addon, R: BufRead + Send + 'static, W: Write + Send + 'static>(
40 mut addon: A,
41 reader: R,
42 writer: W,
43) -> std::io::Result<()> {
44 let pipe = Arc::new(Mutex::new(Pipe {
45 reader,
46 writer,
47 line: String::new(),
48 }));
49 let credentials: CredentialHandle = Arc::new(WireCredentials {
50 pipe: Arc::clone(&pipe),
51 });
52
53 addon.attach_store(Arc::new(WireStore {
56 pipe: Arc::clone(&pipe),
57 }) as StoreHandle);
58
59 loop {
60 let Some(text) = read_line(&pipe)? else {
61 return Ok(()); };
63 let request: Request = match serde_json::from_str(&text) {
64 Ok(r) => r,
65 Err(e) => {
66 send(
70 &pipe,
71 &Reply::Failed {
72 kind: FailureKind::Failed,
73 detail: format!("could not understand that: {e}"),
74 },
75 )?;
76 continue;
77 }
78 };
79
80 let reply = match request {
81 Request::Hello { .. } => Reply::Welcome { version: PROTOCOL },
85 Request::Describe => Reply::Description(describe(&addon)),
86 Request::Availability => Reply::Availability(match addon.availability() {
87 Availability::Ready => AvailabilityDecl::Ready,
88 Availability::Unavailable(detail) => AvailabilityDecl::Unavailable { detail },
89 }),
90 Request::Applies { action } => Reply::Applies {
91 applies: addon.applies(&action),
92 },
93 Request::Status => Reply::Status {
94 status: addon.status(),
95 },
96 Request::LiveChoices { id } => Reply::LiveChoices {
97 choices: addon
98 .live_choices(&id)
99 .into_iter()
100 .map(|c| ChoiceDecl {
101 value: c.value,
102 label: c.label,
103 detail: c.detail,
104 })
105 .collect(),
106 },
107 Request::HeldInputs { action, held } => {
108 addon.held_inputs(&action, &held);
109 Reply::Done
110 }
111 Request::BoundInputs { action, inputs } => {
112 addon.bound_inputs(&action, &inputs);
113 Reply::Done
114 }
115 Request::Configure { values } => {
116 addon.configure(&values, Arc::clone(&credentials));
117 Reply::Done
118 }
119 Request::ReadSignals => Reply::Signals(match addon.read_signals() {
120 Reading::Values(values) => ReadingDecl::Values {
121 values: values
122 .into_iter()
123 .map(|(id, on)| (id.to_owned(), on))
124 .collect(),
125 },
126 Reading::Unavailable(detail) => ReadingDecl::Unavailable { detail },
127 }),
128 Request::Perform {
129 action,
130 params,
131 value,
132 } => perform(&mut addon, &action, ¶ms, value),
133 Request::Shutdown => {
134 send(&pipe, &Reply::Done)?;
135 return Ok(());
136 }
137 };
138 send(&pipe, &reply)?;
139 }
140}
141
142fn perform<A: Addon>(
143 addon: &mut A,
144 action: &str,
145 params: &BTreeMap<String, crate::addon::ParamValue>,
146 value: Option<u16>,
147) -> Reply {
148 if addon.device_actions().iter().any(|d| d.id == action) {
162 return Reply::Failed {
163 kind: FailureKind::Failed,
164 detail: format!(
165 "{action:?} is sent by the device, not by this addon. \
166 This version of Nobble is older than the addon and cannot bind \
167 it as a keystroke; update Nobble."
168 ),
169 };
170 }
171
172 let invocation = match value {
173 Some(v) => Invocation::moved(params, v),
174 None => Invocation::press(params),
175 };
176 match addon.perform(action, &invocation) {
177 Ok(()) => Reply::Done,
178 Err(e) => {
179 let (kind, detail) = match e {
180 AddonError::NoSuchAction(w) => (FailureKind::NoSuchAction, w),
181 AddonError::Unavailable(w) => (FailureKind::Unavailable, w),
182 AddonError::Failed(w) => (FailureKind::Failed, w),
183 };
184 Reply::Failed { kind, detail }
185 }
186 }
187}
188
189fn describe<A: Addon>(addon: &A) -> Description {
195 Description {
196 id: addon.id().to_owned(),
197 name: addon.name().to_owned(),
198 description: addon.description().to_owned(),
199 actions: addon.actions().iter().map(action_decl).collect(),
200 device_actions: addon
201 .device_actions()
202 .iter()
203 .map(device_action_decl)
204 .collect(),
205 choices: addon.choices().iter().map(choices_decl).collect(),
206 signals: addon.signals().iter().map(signal_decl).collect(),
207 settings: addon.settings().iter().map(setting_decl).collect(),
208 permissions: addon.permissions().iter().map(permission_decl).collect(),
209 }
210}
211
212fn permission_decl(p: &Permission) -> PermissionDecl {
213 match *p {
214 Permission::Network { host, reason } => PermissionDecl::Network {
215 host: host.to_owned(),
216 reason: reason.to_owned(),
217 },
218 Permission::Files {
219 path,
220 write,
221 reason,
222 } => PermissionDecl::Files {
223 path: path.to_owned(),
224 write,
225 reason: reason.to_owned(),
226 },
227 Permission::Launch { program, reason } => PermissionDecl::Launch {
228 program: program.to_owned(),
229 reason: reason.to_owned(),
230 },
231 Permission::Credentials { reason } => PermissionDecl::Credentials {
232 reason: reason.to_owned(),
233 },
234 }
235}
236
237fn action_decl(a: &AddonAction) -> ActionDecl {
238 ActionDecl {
239 id: a.id.to_owned(),
240 name: a.name.to_owned(),
241 description: a.description.to_owned(),
242 trigger: a.trigger.as_wire().to_owned(),
243 params: a.params.iter().map(param_decl).collect(),
244 prerequisite: a.prerequisite.map(ToOwned::to_owned),
245 }
246}
247
248fn device_action_decl(d: &DeviceAction) -> DeviceActionDecl {
256 DeviceActionDecl {
257 id: d.id.to_owned(),
258 name: d.name.to_owned(),
259 description: d.description.to_owned(),
260 keystroke: keystroke_decl(d.keystroke),
261 prerequisite: d.prerequisite.map(ToOwned::to_owned),
262 }
263}
264
265fn keystroke_decl(k: DeviceKeystroke) -> KeystrokeDecl {
269 match k {
270 DeviceKeystroke::Tap {
271 key,
272 ctrl,
273 shift,
274 alt,
275 gui,
276 } => KeystrokeDecl::HidTap {
277 key,
278 ctrl,
279 shift,
280 alt,
281 gui,
282 },
283 DeviceKeystroke::Consumer { usage } => KeystrokeDecl::HidConsumer { usage },
284 }
285}
286
287fn param_decl(p: &AddonParam) -> ParamDecl {
288 ParamDecl {
289 id: p.id.to_owned(),
290 name: p.name.to_owned(),
291 description: p.description.to_owned(),
292 kind: p.kind.as_wire().to_owned(),
293 required: p.required,
294 multiple: p.multiple,
295 choices: p.choices.map(ToOwned::to_owned),
296 }
297}
298
299fn choices_decl(c: &AddonChoices) -> ChoicesDecl {
300 ChoicesDecl {
301 id: c.id.to_owned(),
302 name: c.name.to_owned(),
303 live: c.live,
304 values: c
305 .values
306 .iter()
307 .map(|v| ChoiceDecl {
308 value: v.value.to_owned(),
309 label: v.label.to_owned(),
310 detail: String::new(),
314 })
315 .collect(),
316 }
317}
318
319fn signal_decl(s: &AddonSignal) -> SignalDecl {
320 SignalDecl {
321 id: s.id.to_owned(),
322 name: s.name.to_owned(),
323 description: s.description.to_owned(),
324 }
325}
326
327fn setting_decl(s: &AddonSetting) -> SettingDecl {
328 SettingDecl {
329 param: param_decl(&s.param),
330 secret: s.secret,
331 }
332}
333
334struct Pipe<R, W> {
336 reader: R,
337 writer: W,
338 line: String,
339}
340
341fn read_line<R: BufRead, W: Write>(
342 pipe: &Arc<Mutex<Pipe<R, W>>>,
343) -> std::io::Result<Option<String>> {
344 let mut p = pipe
345 .lock()
346 .unwrap_or_else(std::sync::PoisonError::into_inner);
347 p.line.clear();
348 let Pipe { reader, line, .. } = &mut *p;
349 if reader.read_line(line)? == 0 {
350 return Ok(None);
351 }
352 Ok(Some(p.line.trim_end().to_owned()))
353}
354
355fn send<R: BufRead, W: Write, T: serde::Serialize>(
356 pipe: &Arc<Mutex<Pipe<R, W>>>,
357 message: &T,
358) -> std::io::Result<()> {
359 let mut p = pipe
360 .lock()
361 .unwrap_or_else(std::sync::PoisonError::into_inner);
362 let text = serde_json::to_string(message)?;
363 writeln!(p.writer, "{text}")?;
364 p.writer.flush()
365}
366
367struct WireCredentials<R, W> {
375 pipe: Arc<Mutex<Pipe<R, W>>>,
376}
377
378impl<R: BufRead + Send, W: Write + Send> WireCredentials<R, W> {
379 fn exchange(&self, ask: &Ask) -> Option<Answer> {
380 send(&self.pipe, &Reply::Ask(ask.clone())).ok()?;
381 let text = read_line(&self.pipe).ok()??;
382 serde_json::from_str(&text).ok()
383 }
384}
385
386impl<R: BufRead + Send, W: Write + Send> Credentials for WireCredentials<R, W> {
387 fn get(&self, key: &str) -> Option<String> {
388 match self.exchange(&Ask::Get {
389 key: key.to_owned(),
390 })? {
391 Answer::Value { value } => value,
392 Answer::Stored | Answer::Keys { .. } | Answer::Refused { .. } => None,
395 }
396 }
397
398 fn set(&self, key: &str, value: &str) -> Result<(), String> {
399 match self.exchange(&Ask::Set {
400 key: key.to_owned(),
401 value: value.to_owned(),
402 }) {
403 Some(Answer::Stored) => Ok(()),
404 Some(Answer::Refused { detail }) => Err(detail),
405 Some(Answer::Value { .. } | Answer::Keys { .. }) | None => {
406 Err("the daemon did not answer".to_owned())
407 }
408 }
409 }
410
411 fn clear(&self, key: &str) {
412 let _ = self.exchange(&Ask::Clear {
413 key: key.to_owned(),
414 });
415 }
416}
417
418struct WireStore<R, W> {
426 pipe: Arc<Mutex<Pipe<R, W>>>,
427}
428
429impl<R: BufRead + Send, W: Write + Send> WireStore<R, W> {
430 fn exchange(&self, ask: &Ask) -> Option<Answer> {
431 send(&self.pipe, &Reply::Ask(ask.clone())).ok()?;
432 let text = read_line(&self.pipe).ok()??;
433 serde_json::from_str(&text).ok()
434 }
435}
436
437impl<R: BufRead + Send, W: Write + Send> Store for WireStore<R, W> {
438 fn get(&self, key: &str) -> Option<String> {
439 match self.exchange(&Ask::StoreGet {
440 key: key.to_owned(),
441 })? {
442 Answer::Value { value } => value,
443 Answer::Stored | Answer::Keys { .. } | Answer::Refused { .. } => None,
447 }
448 }
449
450 fn set(&self, key: &str, value: &str) -> Result<(), String> {
451 match self.exchange(&Ask::StoreSet {
452 key: key.to_owned(),
453 value: value.to_owned(),
454 }) {
455 Some(Answer::Stored) => Ok(()),
456 Some(Answer::Refused { detail }) => Err(detail),
457 Some(Answer::Value { .. } | Answer::Keys { .. }) | None => {
460 Err("the daemon did not answer".to_owned())
461 }
462 }
463 }
464
465 fn clear(&self, key: &str) {
466 let _ = self.exchange(&Ask::StoreClear {
467 key: key.to_owned(),
468 });
469 }
470
471 fn keys(&self) -> Vec<String> {
472 match self.exchange(&Ask::StoreKeys) {
473 Some(Answer::Keys { keys }) => keys,
474 _ => Vec::new(),
475 }
476 }
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482 use crate::addon::{AddonAction, Availability, Trigger};
483 use std::io::Cursor;
484
485 #[derive(Clone, Default)]
487 struct Recorder(Arc<Mutex<Vec<u8>>>);
488
489 impl Write for Recorder {
490 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
491 self.0
492 .lock()
493 .unwrap_or_else(std::sync::PoisonError::into_inner)
494 .extend_from_slice(buf);
495 Ok(buf.len())
496 }
497 fn flush(&mut self) -> std::io::Result<()> {
498 Ok(())
499 }
500 }
501
502 impl Recorder {
503 fn lines(&self) -> Vec<String> {
504 let bytes = self
505 .0
506 .lock()
507 .unwrap_or_else(std::sync::PoisonError::into_inner)
508 .clone();
509 String::from_utf8(bytes)
510 .expect("utf-8")
511 .lines()
512 .map(str::to_owned)
513 .collect()
514 }
515 }
516
517 const ACTIONS: &[AddonAction] = &[AddonAction {
518 id: "wave",
519 name: "Wave",
520 description: "Says hello.",
521 trigger: Trigger::Momentary,
522 params: &[],
523 ..AddonAction::BASE
524 }];
525
526 const DEVICE_ACTIONS: &[DeviceAction] = &[DeviceAction {
527 id: "salute",
528 name: "Salute",
529 description: "The device sends this one.",
530 keystroke: DeviceKeystroke::Tap {
531 key: 0x10,
532 ctrl: true,
533 shift: true,
534 alt: false,
535 gui: false,
536 },
537 prerequisite: Some("Set this keybind in the other application."),
538 }];
539
540 #[derive(Default)]
541 struct Probe;
542
543 impl Addon for Probe {
544 fn id(&self) -> &'static str {
545 "probe"
546 }
547 fn name(&self) -> &'static str {
548 "Probe"
549 }
550 fn description(&self) -> &'static str {
551 "For tests."
552 }
553 fn actions(&self) -> &'static [AddonAction] {
554 ACTIONS
555 }
556 fn device_actions(&self) -> &'static [DeviceAction] {
557 DEVICE_ACTIONS
558 }
559 fn availability(&self) -> Availability {
560 Availability::Ready
561 }
562 fn perform(&mut self, action: &str, _i: &Invocation<'_>) -> Result<(), AddonError> {
563 match action {
564 "wave" => Ok(()),
565 other => Err(AddonError::NoSuchAction(other.to_owned())),
566 }
567 }
568 fn configure(&mut self, _values: &BTreeMap<String, String>, c: CredentialHandle) {
569 let _ = c.get("token");
573 }
574 }
575
576 fn script(lines: &[&str]) -> Cursor<Vec<u8>> {
577 Cursor::new(lines.join("\n").into_bytes())
578 }
579
580 #[test]
581 fn it_answers_a_handshake_and_describes_itself() {
582 let out = Recorder::default();
583 serve(
584 Probe,
585 script(&[
586 r#"{"ask":"hello","version":{"major":1,"minor":0}}"#,
587 r#"{"ask":"describe"}"#,
588 ]),
589 out.clone(),
590 )
591 .expect("served");
592
593 let lines = out.lines();
594 assert_eq!(
595 lines[0],
596 r#"{"say":"welcome","version":{"major":1,"minor":2}}"#
597 );
598 let described: Reply = serde_json::from_str(&lines[1]).expect("parse");
599 let Reply::Description(d) = described else {
600 panic!("expected a description, got {:?}", lines[1]);
601 };
602 assert_eq!(d.id, "probe");
603 assert_eq!(d.actions.len(), 1);
604 assert_eq!(d.actions[0].trigger, "momentary");
605 assert_eq!(d.actions[0].id, "wave");
607 }
608
609 #[test]
616 fn an_actions_prerequisite_is_carried_into_the_declaration() {
617 const NEEDY: AddonAction = AddonAction {
618 id: "push_to_talk",
619 name: "Push to talk",
620 description: "Holds the key down.",
621 trigger: Trigger::Momentary,
622 prerequisite: Some("Set this keybind in the other application."),
623 ..AddonAction::BASE
624 };
625
626 let decl = action_decl(&NEEDY);
627 assert_eq!(decl.prerequisite.as_deref(), NEEDY.prerequisite);
628 let encoded = serde_json::to_string(&decl).expect("encode");
629 assert!(encoded.contains("Set this keybind"), "{encoded}");
630
631 let plain = serde_json::to_string(&action_decl(&ACTIONS[0])).expect("encode");
634 assert!(!plain.contains("prerequisite"), "{plain}");
635 }
636
637 #[test]
638 fn a_failure_keeps_its_kind() {
639 let out = Recorder::default();
643 serve(
644 Probe,
645 script(&[r#"{"ask":"perform","action":"nope"}"#]),
646 out.clone(),
647 )
648 .expect("served");
649
650 let reply: Reply = serde_json::from_str(&out.lines()[0]).expect("parse");
651 assert_eq!(
652 reply,
653 Reply::Failed {
654 kind: FailureKind::NoSuchAction,
655 detail: "nope".to_owned(),
656 }
657 );
658 }
659
660 #[test]
661 fn a_credential_is_fetched_mid_request_and_names_no_addon() {
662 let out = Recorder::default();
667 serve(
668 Probe,
669 script(&[
670 r#"{"ask":"configure"}"#,
671 r#"{"answer":"value","value":"sekrit"}"#,
672 ]),
673 out.clone(),
674 )
675 .expect("served");
676
677 let lines = out.lines();
678 assert_eq!(
679 lines[0], r#"{"say":"ask","want":"get","key":"token"}"#,
680 "the ask goes out first, and carries no addon name"
681 );
682 assert!(
683 !lines[0].contains("probe"),
684 "an addon must not be able to name whose credential it wants"
685 );
686 assert_eq!(lines[1], r#"{"say":"done"}"#, "then the reply");
687 }
688
689 #[test]
695 fn an_old_daemon_asking_us_to_perform_a_device_action_is_told_why_not() {
696 let out = Recorder::default();
697 serve(
698 Probe,
699 script(&[r#"{"ask":"perform","action":"salute"}"#]),
700 out.clone(),
701 )
702 .expect("served");
703
704 let Reply::Failed { kind, detail } = serde_json::from_str(&out.lines()[0]).expect("parse")
705 else {
706 panic!("expected a failure, got {:?}", out.lines()[0]);
707 };
708 assert_eq!(kind, FailureKind::Failed);
711 assert!(detail.contains("sent by the device"), "{detail}");
712 assert!(detail.contains("update Nobble"), "{detail}");
713 }
714
715 #[test]
720 fn a_device_action_never_reaches_the_addons_own_perform() {
721 let out = Recorder::default();
722 serve(
723 Probe,
724 script(&[r#"{"ask":"perform","action":"salute"}"#]),
725 out.clone(),
726 )
727 .expect("served");
728 assert!(
729 !out.lines()[0].contains("no_such_action"),
730 "the guard let it through: {}",
731 out.lines()[0]
732 );
733 }
734
735 #[test]
736 fn a_frame_it_cannot_parse_is_answered_rather_than_fatal() {
737 let out = Recorder::default();
740 serve(
741 Probe,
742 script(&["not json at all", r#"{"ask":"status"}"#]),
743 out.clone(),
744 )
745 .expect("served");
746
747 let lines = out.lines();
748 assert!(lines[0].contains(r#""say":"failed""#), "{}", lines[0]);
749 assert_eq!(
750 lines[1], r#"{"say":"status","status":null}"#,
751 "and it carries on"
752 );
753 }
754
755 #[test]
756 fn shutdown_ends_it() {
757 let out = Recorder::default();
758 serve(
759 Probe,
760 script(&[r#"{"ask":"shutdown"}"#, r#"{"ask":"status"}"#]),
761 out.clone(),
762 )
763 .expect("served");
764 assert_eq!(
765 out.lines().len(),
766 1,
767 "nothing after shutdown is answered: {:?}",
768 out.lines()
769 );
770 }
771}