@team-agent/installer 0.5.74 → 0.5.77
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/README.md +9 -0
- package/README.zh.md +8 -0
- package/crates/team-agent/src/cli/adapters.rs +23 -96
- package/crates/team-agent/src/cli/emit.rs +35 -7
- package/crates/team-agent/src/cli/mod.rs +63 -42
- package/crates/team-agent/src/cli/models.rs +138 -27
- package/crates/team-agent/src/cli/status_port/brief.rs +1137 -0
- package/crates/team-agent/src/cli/status_port.rs +8 -0
- package/crates/team-agent/src/cli/tests/mod.rs +1 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +29 -24
- package/crates/team-agent/src/cli/tests/status_brief.rs +691 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +8 -2
- package/crates/team-agent/src/layout/worker_env.rs +366 -0
- package/crates/team-agent/src/leader/lease.rs +274 -40
- package/crates/team-agent/src/leader/registry.rs +177 -19
- package/crates/team-agent/src/leader/types.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +93 -5
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1820 -60
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +138 -13
- package/crates/team-agent/src/lifecycle/restart/common.rs +3 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +11 -4
- package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +45 -14
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +1 -0
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +24 -28
- package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +22 -10
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +110 -31
- package/crates/team-agent/src/lifecycle/types.rs +4 -0
- package/crates/team-agent/src/messaging/delivery.rs +200 -29
- package/crates/team-agent/src/messaging/leader_channel.rs +12 -3
- package/crates/team-agent/src/messaging/watchers.rs +48 -4
- package/crates/team-agent/src/state/paths.rs +17 -2
- package/crates/team-agent/src/state/persist.rs +166 -6
- package/crates/team-agent/src/state/projection.rs +18 -2
- package/crates/team-agent/src/state/repository.rs +36 -0
- package/crates/team-agent/src/state/selector.rs +23 -3
- package/crates/team-agent/src/test_support/models_timeout_fixture.rs +122 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +350 -7
- package/crates/team-agent/src/tmux_backend.rs +217 -26
- package/crates/team-agent/src/transport/test_support.rs +18 -3
- package/crates/team-agent/src/transport.rs +99 -0
- package/package.json +4 -4
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
|
|
21
21
|
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
22
22
|
|
|
23
|
+
use std::fmt;
|
|
23
24
|
use std::path::{Path, PathBuf};
|
|
25
|
+
use std::time::{Duration, Instant};
|
|
24
26
|
|
|
25
27
|
use serde::{Deserialize, Serialize};
|
|
26
28
|
use serde_json::{json, Value};
|
|
@@ -100,10 +102,45 @@ pub const AMBIGUOUS_NAMES_FIELD: &str = "ambiguous_names";
|
|
|
100
102
|
/// a fully-qualified form.
|
|
101
103
|
pub const REASON_AMBIGUOUS: &str = "name_ambiguous";
|
|
102
104
|
|
|
105
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
106
|
+
pub enum RegistryRollback {
|
|
107
|
+
Restored,
|
|
108
|
+
Superseded,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
112
|
+
pub enum RegistryRollbackError {
|
|
113
|
+
InvalidReceipt,
|
|
114
|
+
LockUnavailable,
|
|
115
|
+
ReadFailed,
|
|
116
|
+
RestoreFailed,
|
|
117
|
+
DeleteFailed,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
impl fmt::Display for RegistryRollbackError {
|
|
121
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
122
|
+
let reason = match self {
|
|
123
|
+
Self::InvalidReceipt => "invalid receipt",
|
|
124
|
+
Self::LockUnavailable => "registry lock unavailable",
|
|
125
|
+
Self::ReadFailed => "registry receipt read failed",
|
|
126
|
+
Self::RestoreFailed => "registry restore failed",
|
|
127
|
+
Self::DeleteFailed => "registry delete failed",
|
|
128
|
+
};
|
|
129
|
+
f.write_str(reason)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
134
|
+
pub struct RegistryWriteReceipt {
|
|
135
|
+
pub path: PathBuf,
|
|
136
|
+
pub bytes: Vec<u8>,
|
|
137
|
+
}
|
|
138
|
+
|
|
103
139
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
104
140
|
pub struct RegistryWriteOutcome {
|
|
105
141
|
pub status: &'static str,
|
|
106
142
|
pub path: Option<PathBuf>,
|
|
143
|
+
pub receipt: Option<RegistryWriteReceipt>,
|
|
107
144
|
pub team_key: String,
|
|
108
145
|
pub workspace_hash: String,
|
|
109
146
|
pub source: String,
|
|
@@ -242,7 +279,8 @@ pub fn register_binding_from_state_best_effort(
|
|
|
242
279
|
chrono::Utc::now().to_rfc3339(),
|
|
243
280
|
);
|
|
244
281
|
let event_log = crate::event_log::EventLog::new(workspace);
|
|
245
|
-
let
|
|
282
|
+
let write_receipt = write_entry_best_effort_with_receipt(&entry);
|
|
283
|
+
let write_result = write_receipt.as_ref().map(|receipt| receipt.path.clone());
|
|
246
284
|
let status = if let Some(path) = &write_result {
|
|
247
285
|
let _ = event_log.write(
|
|
248
286
|
EVENT_REGISTERED,
|
|
@@ -268,6 +306,7 @@ pub fn register_binding_from_state_best_effort(
|
|
|
268
306
|
Some(RegistryWriteOutcome {
|
|
269
307
|
status,
|
|
270
308
|
path: write_result,
|
|
309
|
+
receipt: write_receipt,
|
|
271
310
|
team_key,
|
|
272
311
|
workspace_hash: entry.workspace_hash,
|
|
273
312
|
source: source.to_string(),
|
|
@@ -281,13 +320,19 @@ fn entry_filename(entry: &LeaderRegistryEntry) -> String {
|
|
|
281
320
|
|
|
282
321
|
/// Registry write is best-effort. Writes to `.<file>.tmp-<pid>-<counter>`
|
|
283
322
|
/// first, then atomically renames to `<workspace_hash>__<team_key>.json`.
|
|
284
|
-
///
|
|
285
|
-
///
|
|
323
|
+
/// Every writer uses the registry lock so conditional rollback can compare
|
|
324
|
+
/// and restore without racing a concurrent publish.
|
|
286
325
|
pub fn write_entry_best_effort(entry: &LeaderRegistryEntry) -> Option<PathBuf> {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
326
|
+
write_entry_best_effort_with_receipt(entry).map(|receipt| receipt.path)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
fn write_entry_best_effort_with_receipt(
|
|
330
|
+
entry: &LeaderRegistryEntry,
|
|
331
|
+
) -> Option<RegistryWriteReceipt> {
|
|
332
|
+
with_registry_lock(|dir| write_entry_locked(dir, entry)).flatten()
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
fn write_entry_locked(dir: &Path, entry: &LeaderRegistryEntry) -> Option<RegistryWriteReceipt> {
|
|
291
336
|
let final_path = dir.join(entry_filename(entry));
|
|
292
337
|
let tmp_name = format!(
|
|
293
338
|
".{}.tmp-{}-{}",
|
|
@@ -296,8 +341,8 @@ pub fn write_entry_best_effort(entry: &LeaderRegistryEntry) -> Option<PathBuf> {
|
|
|
296
341
|
rand_suffix()
|
|
297
342
|
);
|
|
298
343
|
let tmp_path = dir.join(tmp_name);
|
|
299
|
-
let serialized = serde_json::
|
|
300
|
-
if std::fs::write(&tmp_path, serialized).is_err() {
|
|
344
|
+
let serialized = serde_json::to_vec_pretty(entry).ok()?;
|
|
345
|
+
if std::fs::write(&tmp_path, &serialized).is_err() {
|
|
301
346
|
let _ = std::fs::remove_file(&tmp_path);
|
|
302
347
|
return None;
|
|
303
348
|
}
|
|
@@ -305,7 +350,118 @@ pub fn write_entry_best_effort(entry: &LeaderRegistryEntry) -> Option<PathBuf> {
|
|
|
305
350
|
let _ = std::fs::remove_file(&tmp_path);
|
|
306
351
|
return None;
|
|
307
352
|
}
|
|
308
|
-
Some(
|
|
353
|
+
Some(RegistryWriteReceipt {
|
|
354
|
+
path: final_path,
|
|
355
|
+
bytes: serialized,
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/// Restore one registry path only when it still contains this writer's exact
|
|
360
|
+
/// receipt. The comparison and restore/delete are held under the same lock as
|
|
361
|
+
/// ordinary registry writers, so a winner cannot slip in between them.
|
|
362
|
+
pub fn restore_entry_if_current_matches(
|
|
363
|
+
receipt: &RegistryWriteReceipt,
|
|
364
|
+
previous: Option<&[u8]>,
|
|
365
|
+
) -> Result<RegistryRollback, RegistryRollbackError> {
|
|
366
|
+
with_registry_lock_result(|dir| {
|
|
367
|
+
if receipt.path.parent() != Some(dir) {
|
|
368
|
+
return Err(RegistryRollbackError::InvalidReceipt);
|
|
369
|
+
}
|
|
370
|
+
let current = std::fs::read(&receipt.path)
|
|
371
|
+
.map_err(|_| RegistryRollbackError::ReadFailed)?;
|
|
372
|
+
if current != receipt.bytes {
|
|
373
|
+
return Ok(RegistryRollback::Superseded);
|
|
374
|
+
}
|
|
375
|
+
match previous {
|
|
376
|
+
Some(bytes) if replace_entry_bytes_locked(&receipt.path, bytes) => {
|
|
377
|
+
Ok(RegistryRollback::Restored)
|
|
378
|
+
}
|
|
379
|
+
Some(_) => Err(RegistryRollbackError::RestoreFailed),
|
|
380
|
+
None if std::fs::remove_file(&receipt.path).is_ok() => {
|
|
381
|
+
Ok(RegistryRollback::Restored)
|
|
382
|
+
}
|
|
383
|
+
None => Err(RegistryRollbackError::DeleteFailed),
|
|
384
|
+
}
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
fn remove_entry_if_matches(path: &Path, expected: &LeaderRegistryEntry) -> bool {
|
|
389
|
+
with_registry_lock(|_| {
|
|
390
|
+
let Ok(bytes) = std::fs::read(path) else {
|
|
391
|
+
return false;
|
|
392
|
+
};
|
|
393
|
+
let Ok(current) = serde_json::from_slice::<LeaderRegistryEntry>(&bytes) else {
|
|
394
|
+
return false;
|
|
395
|
+
};
|
|
396
|
+
current == *expected && std::fs::remove_file(path).is_ok()
|
|
397
|
+
})
|
|
398
|
+
.unwrap_or(false)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
fn replace_entry_bytes_locked(path: &Path, bytes: &[u8]) -> bool {
|
|
402
|
+
let tmp = path.with_extension(format!(
|
|
403
|
+
"rollback-{}-{}",
|
|
404
|
+
std::process::id(),
|
|
405
|
+
rand_suffix()
|
|
406
|
+
));
|
|
407
|
+
if std::fs::write(&tmp, bytes).is_err() {
|
|
408
|
+
let _ = std::fs::remove_file(&tmp);
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
if std::fs::rename(&tmp, path).is_err() {
|
|
412
|
+
let _ = std::fs::remove_file(&tmp);
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
true
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
struct RegistryLock {
|
|
419
|
+
file: std::fs::File,
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
impl RegistryLock {
|
|
423
|
+
fn acquire(dir: &Path) -> Option<Self> {
|
|
424
|
+
let path = dir.join(".registry.lock");
|
|
425
|
+
let file = std::fs::OpenOptions::new()
|
|
426
|
+
.create(true)
|
|
427
|
+
.read(true)
|
|
428
|
+
.write(true)
|
|
429
|
+
.truncate(false)
|
|
430
|
+
.open(path)
|
|
431
|
+
.ok()?;
|
|
432
|
+
let started = Instant::now();
|
|
433
|
+
loop {
|
|
434
|
+
match crate::platform::file_lock::try_lock_once_nonblocking(&file) {
|
|
435
|
+
Ok(true) => return Some(Self { file }),
|
|
436
|
+
Ok(false) if started.elapsed() < Duration::from_secs(2) => {
|
|
437
|
+
std::thread::sleep(Duration::from_millis(10));
|
|
438
|
+
}
|
|
439
|
+
Ok(false) | Err(_) => return None,
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
impl Drop for RegistryLock {
|
|
446
|
+
fn drop(&mut self) {
|
|
447
|
+
let _ = crate::platform::file_lock::unlock(&self.file);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
fn with_registry_lock<T>(f: impl FnOnce(&Path) -> T) -> Option<T> {
|
|
452
|
+
let dir = registry_dir()?;
|
|
453
|
+
std::fs::create_dir_all(&dir).ok()?;
|
|
454
|
+
let _lock = RegistryLock::acquire(&dir)?;
|
|
455
|
+
Some(f(&dir))
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
fn with_registry_lock_result<T>(
|
|
459
|
+
f: impl FnOnce(&Path) -> Result<T, RegistryRollbackError>,
|
|
460
|
+
) -> Result<T, RegistryRollbackError> {
|
|
461
|
+
let dir = registry_dir().ok_or(RegistryRollbackError::LockUnavailable)?;
|
|
462
|
+
std::fs::create_dir_all(&dir).map_err(|_| RegistryRollbackError::LockUnavailable)?;
|
|
463
|
+
let _lock = RegistryLock::acquire(&dir).ok_or(RegistryRollbackError::LockUnavailable)?;
|
|
464
|
+
f(&dir)
|
|
309
465
|
}
|
|
310
466
|
|
|
311
467
|
fn rand_suffix() -> String {
|
|
@@ -324,14 +480,16 @@ fn rand_suffix() -> String {
|
|
|
324
480
|
/// success — the invariant is "no registry entry after unbind", not
|
|
325
481
|
/// "must have found something to delete".
|
|
326
482
|
pub fn unregister_entry(workspace: &Path, team_key: &str) -> Option<PathBuf> {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
483
|
+
with_registry_lock(|dir| {
|
|
484
|
+
let hash = workspace_hash(workspace);
|
|
485
|
+
let path = dir.join(format!("{hash}__{team_key}.json"));
|
|
486
|
+
if path.exists() {
|
|
487
|
+
std::fs::remove_file(&path).ok()?;
|
|
488
|
+
return Some(path);
|
|
489
|
+
}
|
|
490
|
+
None
|
|
491
|
+
})
|
|
492
|
+
.flatten()
|
|
335
493
|
}
|
|
336
494
|
|
|
337
495
|
/// Read and deserialize every `*.json` file under the registry directory.
|
|
@@ -470,7 +628,7 @@ pub fn list_validated_with_gc() -> Vec<(LeaderRegistryEntry, &'static str, Optio
|
|
|
470
628
|
|| r == "team_not_alive"
|
|
471
629
|
})
|
|
472
630
|
{
|
|
473
|
-
let _ =
|
|
631
|
+
let _ = remove_entry_if_matches(&path, &entry);
|
|
474
632
|
continue;
|
|
475
633
|
}
|
|
476
634
|
out.push((entry, status, reason));
|
|
@@ -419,6 +419,10 @@ pub struct LeaseResult {
|
|
|
419
419
|
/// paths so existing lease JSON stays stable unless a convergence decision
|
|
420
420
|
/// was actually evaluated.
|
|
421
421
|
pub topology_convergence: Option<Value>,
|
|
422
|
+
/// Counts returned by the same attach operation when blocked leader
|
|
423
|
+
/// messages were requeued. Kept typed and team/pane-bound; omitted when
|
|
424
|
+
/// no attach-window debt was observed.
|
|
425
|
+
pub attach_window_failures: Option<Value>,
|
|
422
426
|
}
|
|
423
427
|
|
|
424
428
|
/// Family A 正源 owner 绑定结果(`bind_owner_from_caller_pane` 返回 dict 的 typed 版)。
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
use std::collections::BTreeSet;
|
|
17
17
|
use std::fs::OpenOptions;
|
|
18
18
|
use std::io::{Read, Write};
|
|
19
|
+
#[cfg(test)]
|
|
20
|
+
use std::cell::RefCell;
|
|
19
21
|
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
|
|
20
22
|
use std::path::{Path, PathBuf};
|
|
21
23
|
use std::process::{Command, Stdio};
|
|
@@ -588,6 +590,58 @@ fn executable_file_type(path: &Path) -> Result<PiExecutableFileType, ProviderErr
|
|
|
588
590
|
}
|
|
589
591
|
}
|
|
590
592
|
|
|
593
|
+
#[cfg(test)]
|
|
594
|
+
#[derive(Debug, Default)]
|
|
595
|
+
pub(crate) struct PiCatalogTestObservation {
|
|
596
|
+
pub(crate) spawn_count: usize,
|
|
597
|
+
pub(crate) argv: Vec<String>,
|
|
598
|
+
pub(crate) parent_pid: Option<u32>,
|
|
599
|
+
pub(crate) parent_exit_success: Option<bool>,
|
|
600
|
+
pub(crate) parent_exit_code: Option<i32>,
|
|
601
|
+
pub(crate) reader_timeout: bool,
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
#[cfg(test)]
|
|
605
|
+
struct PiCatalogTestContext {
|
|
606
|
+
receipt: PathBuf,
|
|
607
|
+
helper_mode: Option<String>,
|
|
608
|
+
observation: PiCatalogTestObservation,
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
#[cfg(test)]
|
|
612
|
+
thread_local! {
|
|
613
|
+
static PI_CATALOG_TEST_CONTEXT: RefCell<Option<PiCatalogTestContext>> = RefCell::new(None);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
#[cfg(test)]
|
|
617
|
+
pub(crate) fn with_pi_catalog_test_observation<T>(
|
|
618
|
+
receipt: &Path,
|
|
619
|
+
helper_mode: Option<&str>,
|
|
620
|
+
action: impl FnOnce() -> T,
|
|
621
|
+
) -> (T, PiCatalogTestObservation) {
|
|
622
|
+
PI_CATALOG_TEST_CONTEXT.with(|slot| {
|
|
623
|
+
assert!(slot
|
|
624
|
+
.replace(Some(PiCatalogTestContext {
|
|
625
|
+
receipt: receipt.to_path_buf(),
|
|
626
|
+
helper_mode: helper_mode.map(str::to_owned),
|
|
627
|
+
observation: PiCatalogTestObservation::default(),
|
|
628
|
+
}))
|
|
629
|
+
.is_none());
|
|
630
|
+
let result = action();
|
|
631
|
+
let context = slot.take().expect("Pi catalog test context");
|
|
632
|
+
(result, context.observation)
|
|
633
|
+
})
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
#[cfg(test)]
|
|
637
|
+
fn observe_pi_catalog_test(f: impl FnOnce(&mut PiCatalogTestContext)) {
|
|
638
|
+
PI_CATALOG_TEST_CONTEXT.with(|slot| {
|
|
639
|
+
if let Some(context) = slot.borrow_mut().as_mut() {
|
|
640
|
+
f(context);
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
591
645
|
/// Discover candidates with one PATH-first catalog observation. This deliberately
|
|
592
646
|
/// does not resolve adapters or scan wrapper chains; callers use it only for
|
|
593
647
|
/// rejecting an unqualified role model before lifecycle mutation.
|
|
@@ -596,13 +650,36 @@ pub(crate) fn run_pi_catalog(
|
|
|
596
650
|
timeout: Duration,
|
|
597
651
|
max_bytes: u64,
|
|
598
652
|
) -> Result<Vec<u8>, String> {
|
|
599
|
-
let mut
|
|
600
|
-
|
|
653
|
+
let mut command = Command::new(executable);
|
|
654
|
+
command.arg("--list-models");
|
|
655
|
+
#[cfg(test)]
|
|
656
|
+
observe_pi_catalog_test(|context| {
|
|
657
|
+
context.observation.spawn_count += 1;
|
|
658
|
+
context.observation.argv.push("--list-models".to_string());
|
|
659
|
+
command.env("TEAM_AGENT_MODELS_TIMEOUT_RECEIPT", &context.receipt);
|
|
660
|
+
if let Some(mode) = context.helper_mode.as_deref() {
|
|
661
|
+
command.env("TEAM_AGENT_MODELS_TIMEOUT_MODE", mode);
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
let mut child = command
|
|
601
665
|
.stdin(Stdio::null())
|
|
602
666
|
.stdout(Stdio::piped())
|
|
603
667
|
.stderr(Stdio::null())
|
|
604
668
|
.spawn()
|
|
605
|
-
.map_err(|
|
|
669
|
+
.map_err(|error| {
|
|
670
|
+
#[cfg(test)]
|
|
671
|
+
{
|
|
672
|
+
eprintln!(
|
|
673
|
+
"run_pi_catalog spawn failed kind={:?} raw_os_error={:?}",
|
|
674
|
+
error.kind(),
|
|
675
|
+
error.raw_os_error()
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
let _ = error;
|
|
679
|
+
"Pi executable is unavailable on PATH".to_string()
|
|
680
|
+
})?;
|
|
681
|
+
#[cfg(test)]
|
|
682
|
+
observe_pi_catalog_test(|context| context.observation.parent_pid = Some(child.id()));
|
|
606
683
|
let stdout = child.stdout.take().ok_or_else(|| {
|
|
607
684
|
let _ = child.kill();
|
|
608
685
|
let _ = child.wait();
|
|
@@ -618,8 +695,19 @@ pub(crate) fn run_pi_catalog(
|
|
|
618
695
|
loop {
|
|
619
696
|
match child.try_wait() {
|
|
620
697
|
Ok(Some(status)) => {
|
|
621
|
-
|
|
622
|
-
|
|
698
|
+
#[cfg(test)]
|
|
699
|
+
observe_pi_catalog_test(|context| {
|
|
700
|
+
context.observation.parent_exit_success = Some(status.success());
|
|
701
|
+
context.observation.parent_exit_code = status.code();
|
|
702
|
+
});
|
|
703
|
+
let (result, bytes) = match receiver.recv_timeout(timeout.saturating_sub(start.elapsed())) {
|
|
704
|
+
Ok(value) => value,
|
|
705
|
+
Err(_) => {
|
|
706
|
+
#[cfg(test)]
|
|
707
|
+
observe_pi_catalog_test(|context| context.observation.reader_timeout = true);
|
|
708
|
+
return Err("Pi model catalog command timed out".to_string());
|
|
709
|
+
}
|
|
710
|
+
};
|
|
623
711
|
if !status.success() { return Err("Pi model catalog command failed".into()); }
|
|
624
712
|
result.map_err(|_| "Pi model catalog could not be read".to_string())?;
|
|
625
713
|
if bytes.len() as u64 > max_bytes { return Err("Pi model catalog exceeds the bounded output limit".into()); }
|