1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
//! Upgrade primitives: `cargo install`, binary health-gate, and safe self-restart.
//!
//! Why: Extracted from `update/mod.rs` to keep every file under the 500-line cap
//! while centralising the full upgrade workflow so trusty-memory and trusty-search
//! share identical battle-tested logic.
//!
//! What: Three building blocks plus one orchestrating function:
//! - [`perform_upgrade`] — shell out to `cargo install <name> --locked`,
//! inheriting stdio (for the standalone `upgrade` commands); the
//! [`perform_upgrade_captured`] sibling captures stdio instead, for a
//! caller (trusty-installer) that may be driving its own live terminal
//! display concurrently (#3830).
//! - [`verify_installed_binary`] — health-gate via `<bin> --version`.
//! - [`is_launchd_supervised`] — detect launchd supervision heuristically.
//! - [`upgrade_and_restart`] — compose the three above into the full workflow.
//!
//! Test: `cargo test -p trusty-common --features update-check`
/// How a shelled-out upgrade command's stdout/stderr should be handled.
///
/// Why (#3830): `perform_upgrade` is shared — trusty-memory's and
/// trusty-search's own `upgrade` commands intentionally want cargo's live
/// build output inherited straight through to the operator's terminal. But
/// trusty-installer's `install_all` calls into this same primitive from
/// INSIDE its per-component loop while an interactive `LiveChecklist`
/// (`indicatif::MultiProgress`) may still be actively steady-ticking OTHER
/// not-yet-terminal rows on a background thread — an inherited child writing
/// straight to that same terminal fd races indicatif's redraw and desyncs its
/// line-count tracking, producing the exact duplicate/interleaved-line
/// corruption #3830 reported (traced: `download::try_install_prebuilt`
/// returns `Outcome::Fallback` on ANY failure — network blip, 404,
/// rate-limit, SHA mismatch, not just an unsupported platform — so this path
/// is reachable on a Tier-1 machine too). A typed mode keeps that difference
/// explicit at each call site instead of a bare boolean.
/// What: [`Inherit`] passes the child's stdout/stderr straight through
/// (existing behaviour, unchanged for the standalone upgrade commands);
/// [`Capture`] captures both instead, folding stderr into the returned error
/// on failure so a captured diagnosis is never silently dropped.
/// Test: `run_with_mode_inherit_leaks_to_parent_stdio` (proves `Inherit`
/// really does inherit — the pre-fix behaviour, still correct for its
/// callers), `run_with_mode_capture_never_leaks_to_parent_stdio` (the #3830
/// regression proof for `Capture`).
///
/// [`Inherit`]: UpgradeOutput::Inherit
/// [`Capture`]: UpgradeOutput::Capture
pub
/// Run `cmd` to completion per `mode`, returning `Ok(())` on a zero exit.
///
/// Why: extracted as a free function over an already-configured `Command`
/// (rather than inlined in `perform_upgrade`) so the inherit-vs-capture
/// distinction is directly unit-testable against ANY command — not just
/// `cargo install`, which needs the network and is `#[ignore]`-tagged in CI.
/// What: [`UpgradeOutput::Inherit`] uses `.status()` (inherits stdio, the
/// pre-#3830 behaviour); [`UpgradeOutput::Capture`] uses `.output()`
/// (captures stdio) and folds trimmed stderr into the error on a non-zero
/// exit. `label` is the human-readable command description used in error
/// text (e.g. `` `cargo install foo --locked` ``).
/// Test: `run_with_mode_inherit_leaks_to_parent_stdio`,
/// `run_with_mode_capture_never_leaks_to_parent_stdio`,
/// `run_with_mode_capture_folds_stderr_into_error`.
pub async
/// Run `cargo install <crate_name> --locked` to upgrade the named crate.
///
/// Why: Centralises the upgrade invocation so both trusty-memory and
/// trusty-search call the same battle-tested helper rather than each
/// hand-rolling a `tokio::process::Command`. Keeping it here behind the
/// `update-check` feature ensures no new transitive dependencies are added
/// (tokio::process is already a workspace dep).
///
/// What: Spawns `cargo install <crate_name> --locked`, inheriting the current
/// environment so `CARGO_HOME`, `RUSTUP_TOOLCHAIN`, and PATH resolve normally.
/// Cargo's stdout/stderr are inherited ([`UpgradeOutput::Inherit`]) so the
/// operator can follow progress. Returns `Ok(())` on exit 0; returns `Err`
/// with a descriptive message on non-zero exit.
///
/// For a caller that may be driving its own live terminal display
/// concurrently (trusty-installer's `LiveChecklist`), see
/// [`perform_upgrade_captured`] instead — inheriting here would corrupt that
/// display (#3830).
///
/// Test: `perform_upgrade_fails_cleanly_on_nonexistent_crate` (ignore-tagged —
/// no real cargo-install in CI); manual validation via `trusty-memory upgrade --yes`.
pub async
/// Run `cargo install <crate_name> --locked`, CAPTURING stdout/stderr instead
/// of inheriting them (#3830).
///
/// Why: trusty-installer's `install_all` calls into the cargo-fallback path
/// (`download::try_install_prebuilt` returning `Outcome::Fallback`, which
/// fires on ANY prebuilt-download failure — not just an unsupported
/// platform) from inside its per-component loop, while an interactive
/// `LiveChecklist` may still be actively animating OTHER rows in the
/// background. [`perform_upgrade`]'s inherited stdio would write straight
/// into indicatif's owned terminal region and reproduce the #3830
/// duplicate/interleaved-line corruption; this variant never touches the
/// terminal directly.
///
/// What: Identical to [`perform_upgrade`] except it uses
/// [`UpgradeOutput::Capture`] — the child's stdout/stderr are captured, and a
/// non-zero exit's stderr is folded into the returned error (never silently
/// dropped, never leaked to the live terminal).
///
/// Test: `run_with_mode_capture_never_leaks_to_parent_stdio`,
/// `run_with_mode_capture_folds_stderr_into_error`; the `cargo install`
/// wiring itself mirrors `perform_upgrade`'s (untested beyond that — no real
/// network install in CI).
pub async
/// Resolve the ordered list of directories a binary might have been
/// installed into, given an optional home directory and `CARGO_HOME` value.
///
/// Why: `cargo install` and the prebuilt-download installer path
/// (`trusty-installer::download::default_install_dir`, `~/.local/bin`) place
/// binaries in different directories, and the cargo path honours a
/// `$CARGO_HOME` override (issue #1771). Extracting the rule into a pure
/// function over explicit `home` / `cargo_home` inputs — rather than reading
/// `dirs::home_dir()` / `std::env::var` directly — keeps it testable without
/// mutating global process state for the common case.
///
/// What: Returns, in priority order: `<cargo_home>/bin` when `cargo_home` is
/// `Some` and non-empty, else `<home>/.cargo/bin`; then `<home>/.local/bin`
/// (the prebuilt installer's default). Entries that require `home` are
/// omitted when `home` is `None`.
///
/// Test: `candidate_bin_dirs_prefers_cargo_home_override`,
/// `candidate_bin_dirs_falls_back_to_dot_cargo`,
/// `candidate_bin_dirs_includes_local_bin`,
/// `candidate_bin_dirs_empty_without_home_or_cargo_home`.
pub
/// Probe the freshly-installed binary with `--version` as a health gate.
///
/// Why: Before the daemon self-exits to trigger launchd's KeepAlive respawn,
/// we must confirm the new binary is not corrupt or incompatible. If the
/// health gate fails we keep the old binary running and report the failure
/// clearly — we never exit into a broken binary. The binary may have landed
/// in `~/.cargo/bin` (cargo-install path), `~/.local/bin` (the prebuilt
/// installer's default — see `trusty-installer::download::default_install_dir`),
/// or a custom `$CARGO_HOME/bin`; checking only `~/.cargo/bin` produced false
/// "not installed" reports for prebuilt installs (issue #1771/#1992).
///
/// What: Checks, in order, `$CARGO_HOME/bin/<binary_name>` (or
/// `~/.cargo/bin/<binary_name>` when `CARGO_HOME` is unset), then
/// `~/.local/bin/<binary_name>`, then falls back to PATH via `which`. Spawns
/// `<bin> --version` with a 10-second timeout; returns `Ok(())` if the
/// process exits 0. Returns `Err` on failure, timeout, or missing binary.
///
/// Test: `verify_installed_binary_passes_for_cargo` (ignore-tagged integration);
/// `verify_installed_binary_fails_for_missing_binary`,
/// `verify_installed_binary_finds_binary_in_cargo_bin`,
/// `verify_installed_binary_finds_binary_in_local_bin`,
/// `verify_installed_binary_finds_binary_via_path`,
/// `verify_installed_binary_honours_cargo_home_override`.
pub async
/// Probe a binary at a KNOWN, CONCRETE path with `--version` as a health
/// gate (#3554).
///
/// Why: [`verify_installed_binary`] resolves a bare binary NAME through a
/// priority-ordered directory search that ends in a bare-name PATH `which`
/// fallback — the right shape when the caller genuinely does not know where
/// the binary landed (e.g. after a plain `cargo install`). But a caller that
/// just placed a binary at a SPECIFIC path (the prebuilt-download placement
/// destination, or a resolved `$CARGO_HOME/bin` join) already knows the exact
/// file to verify; re-deriving the path by name afterward is a needless,
/// shadowable extra hop — issue #3554: the web installer wrote 0.19.29 to
/// `~/.local/bin/tm`, then health-gated by NAME, which found a stale
/// 0.19.26 copy at `~/.cargo/bin/tm` first (both because
/// `candidate_bin_dirs` checks `~/.cargo/bin` before `~/.local/bin`, and
/// because a bare-name PATH lookup is inherently shadowable) and reported
/// the health gate "passed" against the WRONG binary. Probing the known
/// absolute path directly makes the health gate immune to any such
/// shadowing — there is no name resolution step for a stale binary to win.
///
/// What: Returns `Err` immediately if `bin_path` does not exist (no PATH
/// fallback — the caller already knows exactly where the binary should be).
/// Otherwise spawns `<bin_path> --version` with the same 10-second timeout as
/// [`verify_installed_binary`]; on a clean exit 0 returns `Ok(<trimmed stdout>)`
/// (the raw `--version` line, so the caller can parse/compare it against an
/// expected version). Returns `Err` on a non-zero exit, spawn failure, or
/// timeout.
///
/// Test: `verify_installed_binary_at_path_reads_exact_binary_despite_path_shadow`
/// (the #3554 regression shape), `verify_installed_binary_at_path_fails_for_missing_binary`.
pub async
/// Return `true` when the current process was started by launchd.
///
/// Why: The self-restart strategy (non-zero exit → launchd KeepAlive
/// respawn) only works when launchd is actually supervising the process.
/// If the daemon was started manually, a non-zero exit would simply terminate
/// with no respawn. Detecting the supervisor lets us choose the right
/// post-upgrade action: supervised → exit(1); unsupervised → print hint.
///
/// What: Heuristic with two prongs on macOS.
///
/// 1. `XPC_SERVICE_NAME` is injected by launchd into every managed job.
/// We guard against interactive terminals (which may inherit env vars)
/// by requiring `TERM_PROGRAM` to be absent.
/// 2. Fallback: PPID == 1 means launchd is the direct parent.
///
/// On non-macOS platforms always returns `false` (launchd is macOS-only).
///
/// Test: `is_launchd_supervised_returns_false_in_test_env` in `tests.rs` verifies
/// that the function returns false in a normal test/terminal environment.
/// Install the new version, health-gate it, then restart or print a hint.
///
/// Why: Concentrates the full upgrade workflow so trusty-memory and
/// trusty-search share identical logic. The function diverges at the final
/// step depending on whether launchd supervision is detected.
///
/// What:
///
/// 1. `perform_upgrade(crate_name)` — run `cargo install <name> --locked`.
/// 2. `verify_installed_binary(binary_name)` — health gate.
/// 3. If supervised by launchd: log to stderr and call
/// `std::process::exit(1)` so launchd respawns the new binary. This
/// path never returns.
/// 4. If not supervised: return `Ok(Some(<hint>))` telling the operator
/// to restart manually.
///
/// Any error short-circuits and returns `Err`; the old binary keeps running.
///
/// `crate_name` is the crates.io package name; `binary_name` is the
/// installed binary name (for trusty-memory and trusty-search they match).
///
/// Test: individual steps (`perform_upgrade`, `verify_installed_binary`,
/// `is_launchd_supervised`) are tested independently in `tests.rs`.
/// The full path is exercised by the CLI upgrade handler and MCP tool.
pub async