running_process/broker/server/handoff/
unix.rs1use std::path::PathBuf;
8
9use super::{
10 HandoffAttemptDecision, HandoffAttemptFailure, HandoffFallbackDecision, HandoffFallbackReason,
11 HandoffToken,
12};
13
14pub const SCM_RIGHTS_TRANSPORT_SUPPORTED: bool =
16 running_process_platform_internal::LEGACY_SCM_RIGHTS_TRANSPORT_SUPPORTED;
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
20pub struct UnixFileDescriptor(i32);
21
22impl UnixFileDescriptor {
23 pub fn new(raw_fd: i32) -> Self {
25 Self(raw_fd)
26 }
27
28 pub fn raw(self) -> i32 {
30 self.0
31 }
32}
33
34#[derive(Clone, Debug, PartialEq, Eq)]
36pub struct UnixHandoffSocket {
37 pub path: PathBuf,
39}
40
41impl UnixHandoffSocket {
42 pub fn new(path: impl Into<PathBuf>) -> Self {
44 Self { path: path.into() }
45 }
46}
47
48#[derive(Clone, Debug, PartialEq, Eq)]
50pub struct ScmRightsAttempt {
51 pub fd: UnixFileDescriptor,
53 pub backend_socket: UnixHandoffSocket,
55 pub handoff_token: HandoffToken,
57}
58
59impl ScmRightsAttempt {
60 pub fn new(
62 fd: UnixFileDescriptor,
63 backend_socket: UnixHandoffSocket,
64 handoff_token: HandoffToken,
65 ) -> Self {
66 Self {
67 fd,
68 backend_socket,
69 handoff_token,
70 }
71 }
72}
73
74#[derive(Clone, Debug, PartialEq, Eq)]
76pub struct ScmRightsSuccess {
77 pub sent_fd: UnixFileDescriptor,
79 pub backend_socket: UnixHandoffSocket,
81 pub handoff_token: HandoffToken,
83}
84
85impl ScmRightsSuccess {
86 pub fn new(
88 sent_fd: UnixFileDescriptor,
89 backend_socket: UnixHandoffSocket,
90 handoff_token: HandoffToken,
91 ) -> Self {
92 Self {
93 sent_fd,
94 backend_socket,
95 handoff_token,
96 }
97 }
98}
99
100pub type ScmRightsResult = Result<ScmRightsSuccess, ScmRightsError>;
102
103pub fn try_send_scm_rights(attempt: &ScmRightsAttempt) -> ScmRightsResult {
109 running_process_platform_internal::legacy_send_fd_to(
110 &attempt.backend_socket.path,
111 attempt.fd.raw(),
112 attempt.handoff_token.as_bytes(),
113 )
114 .map_err(|error| legacy_error(attempt, error, true))?;
115 Ok(ScmRightsSuccess::new(
116 attempt.fd,
117 attempt.backend_socket.clone(),
118 attempt.handoff_token,
119 ))
120}
121
122pub fn try_send_scm_rights_over(socket_fd: i32, attempt: &ScmRightsAttempt) -> ScmRightsResult {
131 running_process_platform_internal::legacy_send_fd_over(
132 socket_fd,
133 attempt.fd.raw(),
134 attempt.handoff_token.as_bytes(),
135 )
136 .map_err(|error| legacy_error(attempt, error, false))?;
137 Ok(ScmRightsSuccess::new(
138 attempt.fd,
139 attempt.backend_socket.clone(),
140 attempt.handoff_token,
141 ))
142}
143
144fn legacy_error(
145 attempt: &ScmRightsAttempt,
146 error: running_process_platform_internal::LegacyHandoffError,
147 connecting: bool,
148) -> ScmRightsError {
149 use running_process_platform_internal::platform::ipc::HandoffTransferErrorKind;
150
151 if let Some((sent_bytes, expected_bytes)) = error.partial_counts() {
152 return ScmRightsError::PartialSend {
153 fd: attempt.fd.raw(),
154 socket: attempt.backend_socket.path.clone(),
155 sent_bytes,
156 expected_bytes,
157 };
158 }
159 match error.kind() {
160 HandoffTransferErrorKind::Unsupported => ScmRightsError::UnsupportedPlatform,
161 HandoffTransferErrorKind::PermissionDenied => ScmRightsError::PermissionDenied {
162 fd: if connecting { -1 } else { attempt.fd.raw() },
163 socket: attempt.backend_socket.path.clone(),
164 },
165 HandoffTransferErrorKind::BackendUnavailable => ScmRightsError::BackendSocketUnavailable {
166 socket: attempt.backend_socket.path.clone(),
167 },
168 HandoffTransferErrorKind::WouldBlock => ScmRightsError::WouldBlock {
169 socket: attempt.backend_socket.path.clone(),
170 },
171 HandoffTransferErrorKind::Failed => ScmRightsError::SendFailed {
172 fd: attempt.fd.raw(),
173 socket: attempt.backend_socket.path.clone(),
174 raw_os_error: error.raw_os_error(),
175 },
176 }
177}
178
179#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
181pub enum ScmRightsError {
182 #[error("SCM_RIGHTS handoff transport is unsupported on this platform")]
184 UnsupportedPlatform,
185 #[error("permission denied passing fd {fd} to backend handoff socket {socket}")]
187 PermissionDenied {
188 fd: i32,
190 socket: PathBuf,
192 },
193 #[error("backend handoff socket is unavailable: {socket}")]
195 BackendSocketUnavailable {
196 socket: PathBuf,
198 },
199 #[error("SCM_RIGHTS send would block for backend handoff socket {socket}")]
201 WouldBlock {
202 socket: PathBuf,
204 },
205 #[error("SCM_RIGHTS send failed for fd {fd} to backend handoff socket {socket}")]
207 SendFailed {
208 fd: i32,
210 socket: PathBuf,
212 raw_os_error: Option<i32>,
214 },
215 #[error("handoff offer delivery failed after passing fd {fd} to backend socket {socket}")]
217 PostTransferDeliveryFailed {
218 fd: i32,
220 socket: PathBuf,
222 },
223 #[error(
225 "SCM_RIGHTS send was partial ({sent_bytes}/{expected_bytes} bytes) for fd {fd} to backend handoff socket {socket}"
226 )]
227 PartialSend {
228 fd: i32,
230 socket: PathBuf,
232 sent_bytes: usize,
234 expected_bytes: usize,
236 },
237 #[error("backend handoff socket {socket} did not acknowledge passed fd")]
239 BackendAckTimeout {
240 socket: PathBuf,
242 },
243}
244
245impl ScmRightsError {
246 pub fn attempt_failure(&self) -> Option<HandoffAttemptFailure> {
248 match self {
249 Self::UnsupportedPlatform => None,
250 Self::PermissionDenied { .. } => Some(HandoffAttemptFailure::PermissionDenied),
251 Self::BackendSocketUnavailable { .. }
252 | Self::WouldBlock { .. }
253 | Self::SendFailed { .. }
254 | Self::PostTransferDeliveryFailed { .. }
255 | Self::PartialSend { .. }
256 | Self::BackendAckTimeout { .. } => Some(HandoffAttemptFailure::BackendAckTimeout),
257 }
258 }
259
260 pub fn fallback_reason(&self) -> HandoffFallbackReason {
262 match self.attempt_failure() {
263 Some(failure) => failure.into(),
264 None => HandoffFallbackReason::ServicePolicyDisabled,
265 }
266 }
267
268 pub fn fallback_decision(&self) -> HandoffFallbackDecision {
270 HandoffFallbackDecision::new(self.fallback_reason())
271 }
272
273 pub fn fallback_attempt_decision(&self) -> HandoffAttemptDecision {
275 HandoffAttemptDecision::FallbackToReconnect(self.fallback_decision())
276 }
277
278 pub fn is_fallback_safe(&self) -> bool {
280 let fallback = self.fallback_decision();
281 fallback.uses_backend_reconnect() && !fallback.sends_client_error()
282 }
283
284 pub fn fd_may_have_reached_backend(&self) -> bool {
290 matches!(self, Self::PostTransferDeliveryFailed { .. })
291 || matches!(self, Self::PartialSend { sent_bytes, .. } if *sent_bytes > 0)
292 }
293}
294
295#[cfg(test)]
296mod platform_neutral_tests {
297 use super::ScmRightsError;
298
299 #[test]
300 fn positive_partial_send_tracks_indeterminate_fd_delivery() {
301 let error = ScmRightsError::PartialSend {
302 fd: 7,
303 socket: "handoff".into(),
304 sent_bytes: 1,
305 expected_bytes: 16,
306 };
307
308 assert!(error.fd_may_have_reached_backend());
309 assert!(error.is_fallback_safe());
310 }
311
312 #[test]
313 fn failed_offer_after_transfer_tracks_backend_ownership() {
314 let error = ScmRightsError::PostTransferDeliveryFailed {
315 fd: 7,
316 socket: "handoff".into(),
317 };
318
319 assert!(error.fd_may_have_reached_backend());
320 assert!(error.is_fallback_safe());
321 }
322}