Skip to main content

running_process_protocol/
lib.rs

1//! Generated protobuf types used by the optional running-process broker client.
2//!
3//! This is an implementation-detail package.  Consumers should use the
4//! client-gated compatibility paths re-exported by `running-process` rather
5//! than depending on this crate directly.
6
7/// Explicit pre-existing broker launch and lifetime-control protocol.
8#[allow(missing_docs)]
9pub mod independent_spawn {
10    include!(concat!(
11        env!("OUT_DIR"),
12        "/running_process.independent_spawn.v1.rs"
13    ));
14}
15
16/// Generated daemon control protocol types.
17#[allow(missing_docs)]
18pub mod daemon {
19    include!(concat!(env!("OUT_DIR"), "/running_process.daemon.v1.rs"));
20}
21
22/// Generated broker protocol types, grouped by frozen wire version.
23pub mod broker {
24    /// Generated v1 broker protocol types.
25    #[allow(missing_docs)]
26    pub mod v1 {
27        include!(concat!(env!("OUT_DIR"), "/running_process.broker.v1.rs"));
28    }
29
30    /// Generated v2 broker protocol types.
31    #[allow(missing_docs)]
32    pub mod v2 {
33        include!(concat!(env!("OUT_DIR"), "/running_process.broker.v2.rs"));
34    }
35}
36
37/// Errors from the broker v1 [`broker::v1::Endpoint`] smart constructors.
38#[derive(Debug, thiserror::Error, PartialEq, Eq)]
39pub enum EndpointNameError {
40    /// The endpoint name or path was empty.
41    #[error("endpoint name must not be empty")]
42    Empty,
43    /// A Windows pipe name carried the `\\\\.\\pipe\\` prefix; endpoint
44    /// paths must be bare because running-process adds the prefix while
45    /// resolving the endpoint.
46    #[error(
47        "windows pipe name must be bare (no \\\\.\\pipe\\ prefix), got {got:?}: \\
48         running-process prepends the prefix when resolving the endpoint"
49    )]
50    PrefixedPipeName {
51        /// The rejected, already-prefixed name.
52        got: String,
53    },
54}
55
56/// Converts a caller's environment policy into the two frozen fields carried
57/// by a v2 [`broker::v2::SessionStart`].
58///
59/// The root crate implements this for its public `EnvironmentPolicy`, keeping
60/// [`broker::v2::SessionStart::with_environment_policy`] an inherent method
61/// for downstream callers.  Keeping the conversion boundary here avoids a
62/// dependency from this optional protocol crate back to the core facade.
63pub trait SessionStartEnvironmentPolicy {
64    /// Return `(environment_policy, clear_inherited_env)` for the wire frame.
65    fn session_start_wire_fields(self) -> (i32, bool);
66}
67
68impl broker::v1::Frame {
69    /// Build a v1 request frame with the frozen envelope defaults.
70    pub fn request(payload_protocol: u32, payload: Vec<u8>) -> Self {
71        Self {
72            envelope_version: 1,
73            kind: broker::v1::FrameKind::Request as i32,
74            payload_protocol,
75            payload,
76            request_id: 0,
77            payload_encoding: broker::v1::PayloadEncoding::None as i32,
78            deadline_unix_ms: 0,
79            traceparent: String::new(),
80            tracestate: String::new(),
81        }
82    }
83
84    /// Build the v1 response frame for `request`.
85    pub fn response_to(request: &Self, payload: Vec<u8>) -> Self {
86        Self {
87            envelope_version: 1,
88            kind: broker::v1::FrameKind::Response as i32,
89            payload_protocol: request.payload_protocol,
90            payload,
91            request_id: request.request_id,
92            payload_encoding: broker::v1::PayloadEncoding::None as i32,
93            deadline_unix_ms: 0,
94            traceparent: request.traceparent.clone(),
95            tracestate: request.tracestate.clone(),
96        }
97    }
98
99    /// Set the correlation request id.
100    #[must_use]
101    pub fn with_request_id(mut self, request_id: u64) -> Self {
102        self.request_id = request_id;
103        self
104    }
105}
106
107impl broker::v1::Endpoint {
108    /// Build a Windows named-pipe endpoint from a bare pipe name.
109    pub fn windows_pipe(
110        namespace_id: impl Into<String>,
111        pipe_name: impl Into<String>,
112    ) -> Result<Self, EndpointNameError> {
113        let pipe_name = pipe_name.into();
114        if pipe_name.is_empty() {
115            return Err(EndpointNameError::Empty);
116        }
117        let lowered = pipe_name.to_ascii_lowercase().replace('/', "\\");
118        if lowered.starts_with("\\\\.\\pipe\\") {
119            return Err(EndpointNameError::PrefixedPipeName { got: pipe_name });
120        }
121        Ok(Self {
122            namespace_id: namespace_id.into(),
123            path: pipe_name,
124        })
125    }
126
127    /// Build a Unix-domain-socket endpoint from a filesystem path.
128    pub fn unix_socket(
129        namespace_id: impl Into<String>,
130        socket_path: impl Into<String>,
131    ) -> Result<Self, EndpointNameError> {
132        let socket_path = socket_path.into();
133        if socket_path.is_empty() {
134            return Err(EndpointNameError::Empty);
135        }
136        Ok(Self {
137            namespace_id: namespace_id.into(),
138            path: socket_path,
139        })
140    }
141}
142
143impl broker::v2::SessionStart {
144    /// Build a contained SESSION request from the caller's current process
145    /// context. Only Unicode environment entries are representable by the
146    /// protobuf string vocabulary.
147    pub fn from_current_process(
148        program: impl Into<String>,
149        args: impl IntoIterator<Item = impl Into<String>>,
150        cwd: impl Into<String>,
151    ) -> Self {
152        Self {
153            program: program.into(),
154            args: args.into_iter().map(Into::into).collect(),
155            cwd: cwd.into(),
156            env: std::env::vars()
157                .map(|(key, value)| broker::v2::SessionEnvVar { key, value })
158                .collect(),
159            clear_inherited_env: true,
160            environment_policy: 3,
161        }
162    }
163
164    /// Select the base environment for this contained session.
165    ///
166    /// This stays inherent on the generated protocol type so existing
167    /// downstream calls do not need to import an extension trait after the
168    /// generated definitions moved into this crate.
169    #[must_use]
170    pub fn with_environment_policy(mut self, policy: impl SessionStartEnvironmentPolicy) -> Self {
171        (self.environment_policy, self.clear_inherited_env) = policy.session_start_wire_fields();
172        self
173    }
174}
175
176#[cfg(test)]
177mod compatibility_tests {
178    use super::broker::v1::{Endpoint, Frame, FrameKind, PayloadEncoding};
179    use super::EndpointNameError;
180
181    #[test]
182    fn frame_constructors_keep_the_frozen_v1_defaults() {
183        let mut request = Frame::request(0x7A63, b"ping".to_vec()).with_request_id(42);
184        assert_eq!(request.envelope_version, 1);
185        assert_eq!(request.kind, FrameKind::Request as i32);
186        assert_eq!(request.payload_encoding, PayloadEncoding::None as i32);
187        assert_eq!(request.request_id, 42);
188
189        request.traceparent = "00-abc-def-01".to_owned();
190        request.tracestate = "vendor=1".to_owned();
191        let response = Frame::response_to(&request, b"pong".to_vec());
192        assert_eq!(response.kind, FrameKind::Response as i32);
193        assert_eq!(response.payload_protocol, request.payload_protocol);
194        assert_eq!(response.request_id, request.request_id);
195        assert_eq!(response.traceparent, request.traceparent);
196        assert_eq!(response.tracestate, request.tracestate);
197    }
198
199    #[test]
200    fn endpoint_constructors_keep_the_public_validation_contract() {
201        let pipe = Endpoint::windows_pipe("svc", "svc-pipe").expect("bare pipe name");
202        assert_eq!(pipe.namespace_id, "svc");
203        assert_eq!(pipe.path, "svc-pipe");
204        assert_eq!(
205            Endpoint::windows_pipe("svc", r"\\.\pipe\svc-pipe"),
206            Err(EndpointNameError::PrefixedPipeName {
207                got: r"\\.\pipe\svc-pipe".to_owned(),
208            })
209        );
210        // soldr#1178: `windows_pipe` lowercases and folds `/` to `\\` before
211        // testing the prefix, so a caller who spells the prefix in either
212        // style or in mixed case must still be rejected. The backslash form
213        // above is the only one the surviving assertions covered; these two
214        // branches had no test after #1151.
215        assert_eq!(
216            Endpoint::windows_pipe("svc", "//./pipe/svc-pipe"),
217            Err(EndpointNameError::PrefixedPipeName {
218                got: "//./pipe/svc-pipe".to_owned(),
219            }),
220            "forward-slash spelling of the prefix must be rejected too"
221        );
222        assert_eq!(
223            Endpoint::windows_pipe("svc", r"\\.\PIPE\svc-pipe"),
224            Err(EndpointNameError::PrefixedPipeName {
225                got: r"\\.\PIPE\svc-pipe".to_owned(),
226            }),
227            "the prefix check is case-insensitive"
228        );
229        assert_eq!(
230            Endpoint::windows_pipe("svc", ""),
231            Err(EndpointNameError::Empty)
232        );
233        assert_eq!(
234            Endpoint::unix_socket("svc", ""),
235            Err(EndpointNameError::Empty)
236        );
237    }
238}