Skip to content

Commit 72c3458

Browse files
zackeesclaude
andcommitted
fix(teensy): address CodeRabbit review on #435
* Two-tier retry timeout: first attempt gets `wait_for_halfkay_timeout_secs` (180 s default — covers the "user walks up and presses the program button" case); subsequent retries get only `flash_timeout_secs` (30 s default) so a wedged device can't burn 15 min of subprocess time before the structured diagnostic surfaces. `flash::run_with_retry` takes both timeouts explicitly. * Normalize the caller's explicit port once: `Some("")` becomes `None` so an empty string never leaks into `DeploymentResult.port` (which the daemon would forward verbatim to the monitor). * On post-flash port-discovery timeout, fall back to the resolved trigger port (the PJRC device we actually flashed) instead of `None` so the same-port-after-flash case still produces a usable port name. * Serialize the three env-var-mutating tests via a shared `TEST_ENV_LOCK` so parallel `cargo test` execution can't race the global process environment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 53f8b5f commit 72c3458

4 files changed

Lines changed: 55 additions & 13 deletions

File tree

crates/fbuild-deploy/src/teensy/first_byte_probe.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ mod tests {
127127

128128
#[test]
129129
fn env_override_parses_zero() {
130+
let _guard = crate::teensy::soft_reboot::TEST_ENV_LOCK
131+
.lock()
132+
.unwrap_or_else(|e| e.into_inner());
130133
std::env::set_var("FBUILD_TEENSY_FIRST_BYTE_TIMEOUT_SECS", "0");
131134
assert_eq!(env_first_byte_timeout_secs_override(), Some(0));
132135
std::env::set_var("FBUILD_TEENSY_FIRST_BYTE_TIMEOUT_SECS", "30");

crates/fbuild-deploy/src/teensy/flash.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,17 @@ impl FlashRunOutcome {
8282
/// Run `teensy_loader_cli` up to `retries + 1` times, sleeping `backoff_ms`
8383
/// between attempts. Stops on the first success.
8484
///
85-
/// `timeout` bounds each individual attempt — not the sum.
85+
/// Two-tier timeout: the *first* attempt is given `first_attempt_timeout` to
86+
/// cover the worst-case "user walks up and presses the program button" path
87+
/// after a baud-134 trigger; every subsequent retry uses the smaller
88+
/// `subsequent_attempt_timeout` since by then HalfKay has either already been
89+
/// observed or the device is wedged in a way another retry won't fix.
8690
pub fn run_with_retry(
8791
cfg: &FlashConfig,
8892
retries: u32,
8993
backoff_ms: u64,
90-
timeout: Duration,
94+
first_attempt_timeout: Duration,
95+
subsequent_attempt_timeout: Duration,
9196
verbose: bool,
9297
) -> Result<FlashRunOutcome> {
9398
let args: Vec<String> = vec![
@@ -109,7 +114,12 @@ pub fn run_with_retry(
109114
let mut attempts: Vec<FlashAttempt> = Vec::with_capacity(total_attempts as usize);
110115

111116
for attempt in 1..=total_attempts {
112-
let result = run_command(&args_ref, None, None, Some(timeout))?;
117+
let attempt_timeout = if attempt == 1 {
118+
first_attempt_timeout
119+
} else {
120+
subsequent_attempt_timeout
121+
};
122+
let result = run_command(&args_ref, None, None, Some(attempt_timeout))?;
113123
let success = result.success();
114124
let exit_code = result.exit_code;
115125
let stdout = result.stdout;
@@ -225,6 +235,9 @@ mod tests {
225235

226236
#[test]
227237
fn env_override_parses() {
238+
let _guard = crate::teensy::soft_reboot::TEST_ENV_LOCK
239+
.lock()
240+
.unwrap_or_else(|e| e.into_inner());
228241
std::env::set_var("FBUILD_TEENSY_FLASH_RETRIES", "9");
229242
assert_eq!(env_flash_retries_override(), Some(9));
230243
std::env::set_var("FBUILD_TEENSY_FLASH_RETRIES", "bogus");

crates/fbuild-deploy/src/teensy/mod.rs

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,15 @@ impl Deployer for TeensyDeployer {
155155
) -> Result<DeploymentResult> {
156156
// ---- 1. Pre-flash port snapshot --------------------------------
157157
let pre_snapshot = port_discovery::snapshot_port_names();
158-
let trigger_port = resolve_trigger_port(port);
158+
// Normalize the caller-supplied port once: treat `Some("")` as
159+
// `None` so we never propagate an empty string into
160+
// `DeploymentResult.port` (the daemon would forward it verbatim to
161+
// the monitor and produce a `failed to open ""` error).
162+
let explicit_port: Option<String> = port
163+
.map(str::trim)
164+
.filter(|s| !s.is_empty())
165+
.map(str::to_string);
166+
let trigger_port = resolve_trigger_port(explicit_port.as_deref());
159167

160168
// Best-effort `usb_type` advisory — log it now so the user sees the
161169
// hint *before* we discover the firmware is silent.
@@ -220,10 +228,15 @@ impl Deployer for TeensyDeployer {
220228
&flash_cfg,
221229
self.flash_retries,
222230
self.flash_retry_backoff_ms,
223-
Duration::from_secs(
224-
self.wait_for_halfkay_timeout_secs
225-
.max(self.flash_timeout_secs),
226-
),
231+
// First attempt may need to wait for the user to press the
232+
// program button on a fresh board — full HalfKay budget.
233+
Duration::from_secs(self.wait_for_halfkay_timeout_secs),
234+
// Subsequent retries: HalfKay was either already observed (the
235+
// baud-134 trigger left a fresh window open) or the device is
236+
// wedged in a way another retry won't fix — use the tighter
237+
// per-flash budget so a wedged board can't burn many minutes
238+
// before falling through to the structured diagnostic.
239+
Duration::from_secs(self.flash_timeout_secs),
227240
self.verbose,
228241
)?;
229242

@@ -237,7 +250,7 @@ impl Deployer for TeensyDeployer {
237250
attempt_count,
238251
flash_outcome.last_exit_code()
239252
),
240-
port: port.map(|p| p.to_string()),
253+
port: explicit_port.clone(),
241254
stdout: flash_outcome.last_stdout().to_string(),
242255
stderr: flash_outcome.last_stderr().to_string(),
243256
outcome: DeployOutcome::FullFlash,
@@ -251,9 +264,12 @@ impl Deployer for TeensyDeployer {
251264
) {
252265
port_discovery::NewPortOutcome::Found(name) => Some(name),
253266
port_discovery::NewPortOutcome::TimedOut => {
254-
// Re-enumeration may simply have used the same port name — fall
255-
// back to the caller-supplied port if any.
256-
port.map(|p| p.to_string())
267+
// Re-enumeration may have reused the same port name (the
268+
// common case on Linux/macOS). Fall back to the explicit
269+
// port the caller asked for, and if none was given, to the
270+
// PJRC port we triggered against — that's the device the
271+
// monitor wants to attach to.
272+
explicit_port.clone().or_else(|| trigger_port.clone())
257273
}
258274
};
259275

crates/fbuild-deploy/src/teensy/soft_reboot.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ pub fn baud_134_trigger_disabled() -> bool {
8080
.unwrap_or(false)
8181
}
8282

83+
/// Shared mutex used by env-var-mutating tests across this `teensy` module
84+
/// tree so they cannot race the global process environment when the test
85+
/// runner schedules them in parallel.
86+
#[cfg(test)]
87+
pub(crate) static TEST_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
88+
8389
#[cfg(test)]
8490
mod tests {
8591
use super::*;
@@ -101,7 +107,11 @@ mod tests {
101107

102108
#[test]
103109
fn disabled_by_env() {
104-
// SAFETY: tests are single-threaded per process here.
110+
// Hold the cross-module lock so concurrent env-mutating tests in
111+
// sibling modules don't race the global process environment.
112+
let _guard = super::TEST_ENV_LOCK
113+
.lock()
114+
.unwrap_or_else(|e| e.into_inner());
105115
std::env::set_var("FBUILD_TEENSY_DISABLE_BAUD_134_TRIGGER", "1");
106116
assert!(baud_134_trigger_disabled());
107117
std::env::set_var("FBUILD_TEENSY_DISABLE_BAUD_134_TRIGGER", "0");

0 commit comments

Comments
 (0)