Skip to main content

mj_controller/
hel_desktop.rs

1//! Process-boundary protocol shared by `mj` and `mj-desktop`.
2
3use std::path::{Path, PathBuf};
4
5use anyhow::{Context, Result};
6use serde::{Deserialize, Serialize};
7
8pub const DESKTOP_LAUNCH_PROTOCOL_VERSION: u32 = 1;
9
10/// Everything the native desktop process needs from the controller process.
11///
12/// The signed cookie is a credential. This value must travel through an
13/// anonymous pipe, never through command-line arguments or environment
14/// variables that process inspection tools commonly expose.
15#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(deny_unknown_fields)]
17pub struct DesktopLaunch {
18    protocol_version: u32,
19    pub viewer_url: String,
20    pub bootstrap_cookie_value: String,
21}
22
23impl std::fmt::Debug for DesktopLaunch {
24    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        formatter
26            .debug_struct("DesktopLaunch")
27            .field("protocol_version", &self.protocol_version)
28            .field("viewer_url", &self.viewer_url)
29            .field("bootstrap_cookie_value", &"[redacted]")
30            .finish()
31    }
32}
33
34impl DesktopLaunch {
35    pub fn new(viewer_url: String, bootstrap_cookie_value: String) -> Self {
36        Self {
37            protocol_version: DESKTOP_LAUNCH_PROTOCOL_VERSION,
38            viewer_url,
39            bootstrap_cookie_value,
40        }
41    }
42
43    pub fn to_json(&self) -> Result<Vec<u8>> {
44        serde_json::to_vec(self).context("serialize desktop launch")
45    }
46
47    pub fn from_json(bytes: &[u8]) -> Result<Self> {
48        let launch: Self = serde_json::from_slice(bytes).context("parse desktop launch")?;
49        anyhow::ensure!(
50            launch.protocol_version == DESKTOP_LAUNCH_PROTOCOL_VERSION,
51            "desktop launch protocol {} is incompatible with supported version {}",
52            launch.protocol_version,
53            DESKTOP_LAUNCH_PROTOCOL_VERSION
54        );
55        Ok(launch)
56    }
57}
58
59/// Resolve a packaged companion beside the currently running executable.
60pub fn sibling_executable(current: &Path, basename: &str) -> PathBuf {
61    current.with_file_name(format!("{basename}{}", std::env::consts::EXE_SUFFIX))
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn desktop_launch_round_trips_without_debugging_its_cookie() {
70        let launch = DesktopLaunch::new(
71            "https://localhost:43210/".into(),
72            "signed-secret-cookie".into(),
73        );
74
75        assert_eq!(
76            DesktopLaunch::from_json(&launch.to_json().unwrap()).unwrap(),
77            launch
78        );
79        let debug = format!("{launch:?}");
80        assert!(debug.contains("[redacted]"), "{debug}");
81        assert!(!debug.contains("signed-secret-cookie"), "{debug}");
82    }
83
84    #[test]
85    fn desktop_launch_rejects_unknown_fields() {
86        let error = DesktopLaunch::from_json(
87            br#"{"protocol_version":1,"viewer_url":"https://localhost/","bootstrap_cookie_value":"secret","extra":true}"#,
88        )
89        .unwrap_err()
90        .to_string();
91        assert!(error.contains("parse desktop launch"), "{error}");
92        assert!(!error.contains("secret"), "{error}");
93    }
94
95    #[test]
96    fn desktop_launch_rejects_an_incompatible_protocol() {
97        let error = DesktopLaunch::from_json(
98            br#"{"protocol_version":99,"viewer_url":"https://localhost/","bootstrap_cookie_value":"secret"}"#,
99        )
100        .unwrap_err()
101        .to_string();
102        assert!(error.contains("protocol 99"), "{error}");
103        assert!(!error.contains("secret"), "{error}");
104    }
105
106    #[test]
107    fn companion_path_replaces_only_the_executable_filename() {
108        let current = Path::new("target/profile/mj");
109        let expected =
110            Path::new("target/profile").join(format!("mj-desktop{}", std::env::consts::EXE_SUFFIX));
111        assert_eq!(sibling_executable(current, "mj-desktop"), expected);
112    }
113}