1use std::{fmt, io, path::PathBuf, process::ExitStatus, time::Duration};
4
5use thruput_hogd_backend::IntroOutcome;
6use thruput_json_rpc_types::NetworkHashText;
7use thruput_x23_transport::ConnectError;
8
9#[cfg(unix)]
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct ChildSignal(i32);
13
14#[cfg(unix)]
15impl ChildSignal {
16 pub const fn from_raw(signal: i32) -> Self {
18 Self(signal)
19 }
20
21 pub const fn raw(self) -> i32 {
23 self.0
24 }
25
26 const fn name(self) -> Option<&'static str> {
27 match self.0 {
28 libc::SIGTERM => Some("SIGTERM"),
29 libc::SIGKILL => Some("SIGKILL"),
30 _ => None,
31 }
32 }
33}
34
35#[cfg(unix)]
36impl fmt::Display for ChildSignal {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 match self.name() {
39 Some(name) => f.write_str(name),
40 None => write!(f, "signal {}", self.raw()),
41 }
42 }
43}
44
45#[derive(Debug, thiserror::Error)]
47#[non_exhaustive]
48pub enum NodeSupervisorError {
49 #[error("invalid local funding state at {}: {reason}", path.display())]
51 LocalFundingState {
52 path: PathBuf,
54 reason: String,
56 },
57 #[error("invalid local airdrop configuration: {reason}")]
59 InvalidLocalAirdropConfig {
60 reason: &'static str,
62 },
63 #[error("invalid protocol override: {source}")]
65 InvalidProtocolOverride {
66 #[from]
68 source: thruput_json_rpc_types::protocol_token::ProtocolTokenWidthError,
69 },
70 #[error("configured hog-devnode binary path does not exist: {}", path.display())]
72 ConfiguredPathMissing {
73 path: PathBuf,
75 },
76
77 #[error("configured hog-devnode binary path is not executable: {}", path.display())]
79 ConfiguredPathNotExecutable {
80 path: PathBuf,
82 },
83
84 #[error("hog-devnode binary not found")]
86 HogdDevNotFound,
87
88 #[error("failed to resolve current executable path: {source}")]
90 CurrentExe {
91 #[source]
93 source: io::Error,
94 },
95
96 #[error("failed to spawn child process `{program}`: {source}")]
98 Spawn {
99 program: String,
101 #[source]
103 source: io::Error,
104 },
105
106 #[error("invalid hogd-dev bootstrap manifest request: {reason}")]
108 InvalidBootstrapManifestConfig {
109 reason: String,
111 },
112
113 #[error("failed to create hogd-dev bootstrap manifest temp dir: {source}")]
115 BootstrapManifestTempDir {
116 #[source]
118 source: io::Error,
119 },
120
121 #[error("failed to serialize hogd-dev bootstrap manifest: {source}")]
123 BootstrapManifestSerialize {
124 #[source]
126 source: serde_json::Error,
127 },
128
129 #[error("failed to write hogd-dev bootstrap manifest {}: {source}", path.display())]
131 BootstrapManifestWrite {
132 path: PathBuf,
134 #[source]
136 source: io::Error,
137 },
138
139 #[error("child process `{program}` did not expose stdout")]
141 MissingStdout {
142 program: String,
144 },
145
146 #[error("child process `{program}` did not expose stderr")]
148 MissingStderr {
149 program: String,
151 },
152
153 #[error("child process `{program}` did not report readiness within {timeout:?}")]
155 ReadinessTimeout {
156 program: String,
158 timeout: Duration,
160 },
161
162 #[error("child process `{program}` wrote a readiness line larger than {max_bytes} bytes")]
164 ReadinessTooLarge {
165 program: String,
167 max_bytes: usize,
169 },
170
171 #[error("child process `{program}` exited before readiness with status {status}")]
173 ExitedBeforeReady {
174 program: String,
176 status: ExitStatus,
178 },
179
180 #[error("failed to read readiness from child process `{program}`: {source}")]
182 ReadinessRead {
183 program: String,
185 #[source]
187 source: io::Error,
188 },
189
190 #[error("invalid readiness JSON from child process `{program}`: {source}")]
192 InvalidReadinessJson {
193 program: String,
195 line: String,
197 #[source]
199 source: serde_json::Error,
200 },
201
202 #[error(
204 "invalid readiness schema version from child process `{program}`: expected {expected}, got {actual}"
205 )]
206 InvalidReadinessSchemaVersion {
207 program: String,
209 expected: u32,
211 actual: u32,
213 },
214
215 #[error("invalid readiness status from child process `{program}`: {status}")]
217 InvalidReadinessStatus {
218 program: String,
220 status: String,
222 },
223
224 #[error(
226 "failed to read runtime manifest `{path}` advertised by child process `{program}`: {source}"
227 )]
228 RuntimeManifestRead {
229 program: String,
231 path: String,
233 #[source]
235 source: io::Error,
236 },
237
238 #[error("unsafe runtime manifest `{path}` advertised by child process `{program}`: {reason}")]
240 RuntimeManifestUnsafePermissions {
241 program: String,
243 path: String,
245 reason: String,
247 },
248
249 #[error("invalid runtime manifest hash from child process `{program}`: {hash}")]
251 InvalidRuntimeManifestHash {
252 program: String,
254 hash: String,
256 },
257
258 #[error(
260 "runtime manifest hash mismatch from child process `{program}` for `{path}`: expected {expected}, got {actual}"
261 )]
262 RuntimeManifestHashMismatch {
263 program: String,
265 path: String,
267 expected: String,
269 actual: String,
271 },
272
273 #[error("invalid runtime manifest from child process `{program}`: {reason}")]
275 InvalidRuntimeManifest {
276 program: String,
278 reason: String,
280 },
281
282 #[error(
284 "hogd-dev runtime manifest network hash mismatch: bootstrap requested {expected}, runtime advertised {observed}"
285 )]
286 RuntimeNetworkHashMismatch {
287 expected: NetworkHashText,
289 observed: NetworkHashText,
291 },
292
293 #[error(
295 "hogd-dev runtime manifest protocol mismatch: supervisor requested {expected}, runtime advertised {observed:?}"
296 )]
297 RuntimeProtocolMismatch {
298 expected: String,
300 observed: Option<String>,
302 },
303
304 #[cfg(unix)]
306 #[error("failed to send {signal} to child process group `{program}`: {source}")]
307 Signal {
308 program: String,
310 signal: ChildSignal,
312 #[source]
314 source: io::Error,
315 },
316
317 #[error("failed to query child process `{program}` status: {source}")]
319 Status {
320 program: String,
322 #[source]
324 source: io::Error,
325 },
326
327 #[error("failed to wait for child process `{program}`: {source}")]
329 Wait {
330 program: String,
332 #[source]
334 source: io::Error,
335 },
336
337 #[error("failed to stop child process `{program}`: {source}")]
339 Stop {
340 program: String,
342 #[source]
344 source: io::Error,
345 },
346
347 #[error("failed to bind JSON-RPC server to {address}: {source}")]
349 RpcBind {
350 address: String,
352 #[source]
354 source: io::Error,
355 },
356
357 #[cfg(unix)]
359 #[error("failed to raise JSON-RPC nofile limit toward {target}: {source}")]
360 RpcNofileLimit {
361 target: u64,
363 #[source]
365 source: io::Error,
366 },
367
368 #[cfg(unix)]
370 #[error("JSON-RPC nofile hard limit {hard} is below requested target {target}")]
371 RpcNofileHardLimit {
372 target: u64,
374 hard: u64,
376 },
377
378 #[error("failed to read JSON-RPC bound address: {source}")]
380 RpcLocalAddr {
381 #[source]
383 source: io::Error,
384 },
385
386 #[error("failed to wait for supervisor stop signal: {source}")]
388 StopSignal {
389 #[source]
391 source: io::Error,
392 },
393
394 #[error("failed to connect hogd-dev SchedulerCoord endpoint {endpoint}: {source}")]
396 SchedulerConnect {
397 endpoint: std::net::SocketAddr,
399 #[source]
401 source: ConnectError,
402 },
403
404 #[error("failed to connect hogd-dev SchedGateway endpoint {endpoint}: {source}")]
406 SchedulerGatewayConnect {
407 endpoint: std::net::SocketAddr,
409 #[source]
411 source: ConnectError,
412 },
413
414 #[error(
416 "hogd-dev scheduler network hash mismatch: runtime manifest expected {expected}, scheduler reported {observed}"
417 )]
418 SchedulerNetworkHashMismatch {
419 expected: NetworkHashText,
421 observed: NetworkHashText,
423 },
424
425 #[error(
427 "hogd-dev scheduler planes disagree at startup: coordinator {coordinator}, gateway {gateway}"
428 )]
429 SchedulerPlaneTipMismatch {
430 coordinator: String,
432 gateway: String,
434 },
435
436 #[error("hogd-dev scheduler RPC `{op}` failed: {source}")]
438 SchedulerRpc {
439 op: &'static str,
441 #[source]
443 source: rpc_driver::RpcError,
444 },
445
446 #[error("hogd-dev scheduler gateway RPC `{op}` failed: {source}")]
448 SchedulerGatewayRpc {
449 op: &'static str,
451 #[source]
453 source: rpc_driver::RpcError,
454 },
455
456 #[error("hogd-dev scheduler rejected `{op}`: {result}")]
458 SchedulerRejected {
459 op: &'static str,
461 result: String,
463 },
464
465 #[error("failed to connect hogd-dev IndexerBlockExplorer endpoint {endpoint}: {source}")]
467 IndexerConnect {
468 endpoint: std::net::SocketAddr,
470 #[source]
472 source: ConnectError,
473 },
474
475 #[error("hogd-dev indexer RPC `{op}` failed: {error}")]
477 IndexerRpc {
478 op: &'static str,
480 error: String,
482 },
483
484 #[error("failed to connect hogd-dev DbGateway endpoint {endpoint}: {source}")]
486 DbGatewayConnect {
487 endpoint: std::net::SocketAddr,
489 #[source]
491 source: ConnectError,
492 },
493
494 #[error("hogd-dev DbGateway RPC `{op}` failed: {error}")]
496 DbGatewayRpc {
497 op: &'static str,
499 error: String,
501 },
502
503 #[error("failed to connect hogd-dev ViewGateway endpoint {endpoint}: {source}")]
505 ViewGatewayConnect {
506 endpoint: std::net::SocketAddr,
508 #[source]
510 source: ConnectError,
511 },
512
513 #[error("hogd-dev ViewGateway RPC `{op}` failed: {error}")]
515 ViewGatewayRpc {
516 op: &'static str,
518 error: String,
520 },
521
522 #[error(
524 "{}",
525 intro_rejected_message(
526 service,
527 *outcome,
528 outcome_name,
529 *is_protocol_mismatch,
530 protocol,
531 expected_protocol.as_deref()
532 )
533 )]
534 ProtocolIntroRejected {
535 service: &'static str,
537 outcome: i32,
539 outcome_name: &'static str,
541 is_protocol_mismatch: bool,
543 protocol: String,
545 expected_protocol: Option<String>,
547 },
548
549 #[error("hogd-dev PID file operation failed at {}: {source}", path.display())]
551 PidFile {
552 path: PathBuf,
554 #[source]
556 source: io::Error,
557 },
558
559 #[error(
561 "thruput local node already appears to be running (PID file {}, hogd-dev pid {pid:?}); stop it before starting another node",
562 path.display()
563 )]
564 LocalNodeAlreadyRunning {
565 path: PathBuf,
567 pid: Option<u32>,
569 },
570
571 #[error(
573 "child process `{program}` did not fully stop after SIGKILL (process group {process_group:?}); preserving its PID file to prevent unsafe restart"
574 )]
575 ShutdownIncomplete {
576 program: String,
578 process_group: Option<i32>,
580 },
581
582 #[error("persistent hog-devnode state is only supported on Unix platforms")]
584 PersistentStateUnsupported,
585
586 #[error("hog-devnode data directory operation failed at {}: {source}", path.display())]
588 DataDir {
589 path: PathBuf,
591 #[source]
593 source: io::Error,
594 },
595}
596
597impl NodeSupervisorError {
598 pub(crate) fn intro_rejected(
605 outcome: IntroOutcome,
606 protocol: &str,
607 expected_protocol: Option<&str>,
608 ) -> Self {
609 Self::ProtocolIntroRejected {
610 service: outcome.service(),
611 outcome: outcome.code(),
612 outcome_name: outcome.name(),
613 is_protocol_mismatch: outcome.mismatched_protocol(),
614 protocol: protocol.to_string(),
615 expected_protocol: expected_protocol.map(str::to_string),
616 }
617 }
618}
619
620fn intro_rejected_message(
621 service: &str,
622 outcome: i32,
623 outcome_name: &str,
624 is_protocol_mismatch: bool,
625 protocol: &str,
626 expected_protocol: Option<&str>,
627) -> String {
628 if is_protocol_mismatch {
629 return match expected_protocol {
630 Some(expected_protocol) => format!(
631 "{service} intro mismatch: thruput node speaks protocol {protocol}, \
632 hogd-dev expects {expected_protocol} ({outcome_name}; binary is built against \
633 a different protocol commit)"
634 ),
635 None => format!(
636 "{service} intro mismatch: thruput node speaks protocol {protocol}, \
637 but hogd-dev rejected it ({outcome_name})"
638 ),
639 };
640 }
641
642 format!(
643 "hogd-dev {service} intro rejected with outcome {outcome_name} \
644 (discriminant {outcome}); thruput node speaks protocol {protocol}"
645 )
646}