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