mj_controller/worker_client/
transport.rs1use super::*;
2
3pub(super) fn log_relay_client_failure(
8 client: &RelayClient,
9 operation: &str,
10 request_id: &str,
11 error: &anyhow::Error,
12) {
13 let rejection = error.chain().find_map(|cause| {
14 cause
15 .downcast_ref::<RelayRejected>()
16 .map(|rejected| &rejected.0)
17 });
18 let transport_dead = RelayTransportDead::marks(error);
19 match rejection {
20 Some(rejection) => tracing::warn!(
21 session_id = %client.session_id,
22 relay_version = %client.relay_version,
23 %operation,
24 %request_id,
25 relay_error_code = ?rejection.code,
26 relay_retryable = rejection.retryable,
27 transport_dead,
28 error = %error,
29 "relay request rejected"
30 ),
31 None => tracing::warn!(
32 session_id = %client.session_id,
33 relay_version = %client.relay_version,
34 %operation,
35 %request_id,
36 transport_dead,
37 error = %error,
38 "relay request failed"
39 ),
40 }
41}
42
43impl Drop for RelayClient {
44 fn drop(&mut self) {
45 drop(self.input.take());
50 let Some(child) = self.child.take() else {
51 return;
52 };
53 let session_id = self.session_id.clone();
54 if let Err(error) = std::thread::Builder::new()
55 .name("hel-relay-reaper".into())
56 .spawn(move || reap_dropped_relay_proxy(child, session_id))
57 {
58 tracing::warn!(
59 session_id = %self.session_id,
60 %error,
61 "could not start dropped relay proxy reaper"
62 );
63 }
64 }
65}
66
67pub(super) fn reap_dropped_relay_proxy(mut child: Child, session_id: String) {
70 let deadline = Instant::now() + RELAY_PROXY_DETACH_GRACE;
71 loop {
72 match child.try_wait() {
73 Ok(Some(status)) => {
74 if !status.success() {
75 tracing::warn!(
76 %session_id,
77 %status,
78 "dropped relay proxy exited unsuccessfully"
79 );
80 }
81 return;
82 }
83 Ok(None) if Instant::now() < deadline => {
84 std::thread::sleep(RELAY_PROXY_REAP_POLL);
85 }
86 Ok(None) => break,
87 Err(error) => {
88 tracing::warn!(%session_id, %error, "could not reap dropped relay proxy");
89 return;
90 }
91 }
92 }
93
94 if let Err(error) = child.start_kill()
95 && error.kind() != std::io::ErrorKind::NotFound
96 {
97 tracing::warn!(%session_id, %error, "could not stop dropped relay proxy");
98 return;
99 }
100 let deadline = Instant::now() + RELAY_PROXY_DETACH_GRACE;
101 loop {
102 match child.try_wait() {
103 Ok(Some(_)) => return,
104 Ok(None) if Instant::now() < deadline => {
105 std::thread::sleep(RELAY_PROXY_REAP_POLL);
106 }
107 Ok(None) => {
108 tracing::warn!(%session_id, "stopped relay proxy could not be reaped in time");
109 return;
110 }
111 Err(error) => {
112 tracing::warn!(%session_id, %error, "could not reap stopped relay proxy");
113 return;
114 }
115 }
116 }
117}
118
119pub(super) fn credential_snapshot(payload: RelayResponsePayload) -> Result<CredentialSnapshot> {
120 match payload {
121 RelayResponsePayload::CredentialState {
122 present,
123 fingerprint,
124 freshness_epoch_ms,
125 } => Ok(CredentialSnapshot {
126 present,
127 fingerprint,
128 freshness_epoch_ms,
129 }),
130 _ => bail!("relay returned an unexpected credential state response"),
131 }
132}
133
134pub(super) fn skills_sync_state(
135 payload: RelayResponsePayload,
136) -> Result<mj_core::skills::SkillsSyncState> {
137 match payload {
138 RelayResponsePayload::SkillsState {
139 present,
140 fingerprint,
141 } => Ok(mj_core::skills::SkillsSyncState {
142 present,
143 fingerprint,
144 }),
145 _ => bail!("relay returned an unexpected skills state response"),
146 }
147}
148
149pub(super) fn github_token_snapshot(
150 payload: RelayResponsePayload,
151) -> Result<mj_core::credentials::GithubTokenSnapshot> {
152 match payload {
153 RelayResponsePayload::GithubTokenState {
154 present,
155 fingerprint,
156 } => Ok(mj_core::credentials::GithubTokenSnapshot {
157 present,
158 fingerprint,
159 }),
160 _ => bail!("relay returned an unexpected GitHub token state response"),
161 }
162}
163
164pub(super) async fn read_bounded_frame(
165 reader: &mut (impl AsyncBufRead + Unpin),
166 kind: ExchangeKind,
167) -> Result<Option<String>> {
168 read_bounded_frame_with_limit(reader, MAX_FRAME_BYTES, kind).await
169}
170
171pub(super) async fn read_bounded_frame_with_limit(
172 reader: &mut (impl AsyncBufRead + Unpin),
173 maximum_bytes: usize,
174 kind: ExchangeKind,
175) -> Result<Option<String>> {
176 use mj_core::bounded_frame::{BoundedFrame, BoundedFrameError};
177 let mut frame = match mj_core::bounded_frame::read_bounded_frame(reader, maximum_bytes).await {
181 Ok(BoundedFrame::Line(frame)) => frame,
182 Ok(BoundedFrame::End) => return Ok(None),
183 Ok(BoundedFrame::Truncated(_)) => {
184 return Err(anyhow::Error::new(RelayTransportDead::during_exchange(
185 "relay proxy disconnected in the middle of a response frame",
186 kind,
187 )));
188 }
189 Err(BoundedFrameError::Io(error)) => {
190 return Err(RelayTransportDead::from_io(error, kind).into());
191 }
192 Err(BoundedFrameError::TooLarge) => bail!("relay response frame is too large"),
193 };
194 if frame.last() == Some(&b'\r') {
195 frame.pop();
196 }
197 String::from_utf8(frame)
198 .context("relay response is not UTF-8")
199 .map(Some)
200}
201
202pub(super) fn clip_catch_up_page(
203 page: RelayAttachment,
204 previous: &RelayCursor,
205 frontier: &RelayCursor,
206) -> Result<RelayEventPage> {
207 if previous.ordinal > frontier.ordinal {
208 bail!("relay catch-up starts beyond its fixed frontier");
209 }
210 if previous.ordinal == frontier.ordinal {
211 if previous != frontier {
212 bail!("relay catch-up cursor digest differs from its fixed frontier");
213 }
214 if !page.events.is_empty() || page.through_ordinal != previous.ordinal {
215 bail!("relay attachment advanced beyond its advertised frontier");
216 }
217 return Ok(RelayEventPage {
218 events: Vec::new(),
219 through_ordinal: previous.ordinal,
220 through_digest: previous.digest.clone(),
221 });
222 }
223 if page.through_ordinal <= previous.ordinal || page.events.is_empty() {
224 bail!("relay catch-up page did not advance");
225 }
226 if page.through_ordinal <= frontier.ordinal {
227 let through = RelayCursor {
228 ordinal: page.through_ordinal,
229 digest: page.through_digest.clone(),
230 };
231 if through.ordinal == frontier.ordinal && through != *frontier {
232 bail!("relay catch-up page digest differs from its fixed frontier");
233 }
234 return Ok(RelayEventPage {
235 events: page.events,
236 through_ordinal: through.ordinal,
237 through_digest: through.digest,
238 });
239 }
240
241 let events = page
242 .events
243 .into_iter()
244 .take_while(|event| event.ordinal <= frontier.ordinal)
245 .collect::<Vec<_>>();
246 let reached = events
247 .last()
248 .map(|event| RelayCursor {
249 ordinal: event.ordinal,
250 digest: event.digest.clone(),
251 })
252 .ok_or_else(|| anyhow!("relay catch-up page skipped its fixed frontier"))?;
253 if reached != *frontier {
254 bail!("relay catch-up page does not contain its fixed frontier");
255 }
256 Ok(RelayEventPage {
257 events,
258 through_ordinal: reached.ordinal,
259 through_digest: reached.digest,
260 })
261}
262
263pub(super) fn decode_relay_response(
264 line: &str,
265 request_id: &str,
266 protocol: u32,
267) -> Result<RelayResponsePayload> {
268 let response: RelayResponseEnvelope =
269 serde_json::from_str(line).context("decode relay response")?;
270 if response.request_id != request_id {
271 bail!(
272 "relay response ID mismatch: expected {request_id}, got {}",
273 response.request_id
274 );
275 }
276 if response.protocol_version != protocol {
277 bail!(
278 "relay response protocol mismatch: expected {protocol}, got {}",
279 response.protocol_version
280 );
281 }
282 match response.body {
283 RelayResponseBody::Ok { payload } => Ok(payload),
284 RelayResponseBody::Error { error } => Err(RelayRejected(error).into()),
285 }
286}
287
288pub(super) fn decode_relay_hello_response(
289 line: &str,
290 request_id: &str,
291) -> Result<RelayResponsePayload> {
292 let response: RelayResponseEnvelope =
293 serde_json::from_str(line).context("decode relay hello response")?;
294 if response.request_id != request_id {
295 bail!(
296 "relay response ID mismatch: expected {request_id}, got {}",
297 response.request_id
298 );
299 }
300 match response.body {
301 RelayResponseBody::Ok {
302 payload: payload @ RelayResponsePayload::Hello { negotiated, .. },
303 } => {
304 if response.protocol_version != negotiated {
305 bail!(
306 "relay hello envelope uses protocol {}, negotiated {negotiated}",
307 response.protocol_version
308 );
309 }
310 Ok(payload)
311 }
312 RelayResponseBody::Ok { .. } => bail!("relay returned an unexpected hello response"),
313 RelayResponseBody::Error { error } => Err(RelayRejected(error).into()),
314 }
315}