Skip to content

Commit 27d5e26

Browse files
committed
fix: replay cached entries to reporter on cache hit; per-company panic labels (PR #458)
1 parent fe0a2e8 commit 27d5e26

2 files changed

Lines changed: 82 additions & 17 deletions

File tree

crates/pet-windows-registry/src/environments.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,11 @@ fn get_registry_pythons_for_hive(
141141
let handles: Vec<_> = companies
142142
.into_iter()
143143
.map(|(company, company_key)| {
144-
s.spawn(move || {
144+
// Build the panic-warning label up-front so a panicking
145+
// company thread is identifiable in logs (issue #454
146+
// Copilot review feedback).
147+
let label = format!("{name}\\Software\\Python\\{company}");
148+
let handle = s.spawn(move || {
145149
// Trace order is intentionally relaxed: companies are
146150
// walked in parallel, so this line interleaves with the
147151
// others from the same hive.
@@ -153,12 +157,13 @@ fn get_registry_pythons_for_hive(
153157
conda_locator,
154158
reporter,
155159
)
156-
})
160+
});
161+
(label, handle)
157162
})
158163
.collect();
159164
handles
160165
.into_iter()
161-
.map(|h| join_or_warn(h.join(), "per-company walk"))
166+
.map(|(label, h)| join_or_warn(h.join(), &label))
162167
.collect()
163168
});
164169

crates/pet-windows-registry/src/lib.rs

Lines changed: 74 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -124,12 +124,35 @@ impl Locator for WindowsRegistry {
124124

125125
#[cfg(windows)]
126126
fn find(&self, reporter: &dyn Reporter) {
127-
// We no longer reset `search_result` here: `find_with_cache` already
128-
// returns the cached result on a hit, and `find()` is invoked on
129-
// transient locators per refresh, so the cache is empty by
130-
// construction the first time. Re-clearing forced every refresh to
131-
// re-walk both registry hives, each of which is intercepted by
132-
// Windows Defender (issue #454).
127+
// We no longer reset `search_result` here: the cache may have been
128+
// populated via `sync_refresh_state_from` between refreshes, and
129+
// `find()` is invoked on transient locators per refresh, so on the
130+
// first refresh the cache is empty by construction. Re-clearing
131+
// forced every refresh to re-walk both registry hives, each of
132+
// which is intercepted by Windows Defender (issue #454).
133+
//
134+
// On a cache hit `get_registry_pythons` is NOT called, so we must
135+
// replay the cached environments/managers to the reporter
136+
// ourselves — otherwise WindowsRegistry discoveries would silently
137+
// disappear on every refresh after the first.
138+
let cached = {
139+
let result = self
140+
.search_result
141+
.lock()
142+
.expect("search_result mutex poisoned");
143+
result.clone()
144+
};
145+
if let Some(cached) = cached {
146+
for manager in &cached.managers {
147+
reporter.report_manager(manager);
148+
}
149+
for env in &cached.environments {
150+
reporter.report_environment(env);
151+
}
152+
return;
153+
}
154+
// Cache miss: walk the registry. `get_registry_pythons` reports
155+
// inline as it discovers entries, so no separate replay is needed.
133156
let _ = self.find_with_cache(Some(reporter));
134157
}
135158
#[cfg(unix)]
@@ -328,26 +351,48 @@ mod tests {
328351
/// implementation called `self.clear()` first, which forced every
329352
/// `refresh` RPC to re-walk both registry hives — a Defender-intercepted
330353
/// hot path tracked in #454. This test pins down the new contract:
331-
/// pre-populate the cache, run `find()`, and assert the original entries
332-
/// survived (i.e. `find_with_cache` short-circuited on the cache hit).
354+
/// pre-populate the cache, run `find()`, and assert (a) the original
355+
/// entries survived (i.e. the cache was not cleared) and (b) the
356+
/// reporter was notified with the cached environments and managers,
357+
/// so cached results are still observable to refresh consumers.
333358
#[cfg(windows)]
334359
#[test]
335360
fn test_find_reuses_cached_results_within_locator_lifetime() {
336361
use pet_core::manager::EnvManager;
337362
use pet_core::python_environment::PythonEnvironment;
338363
use pet_core::reporter::Reporter;
339364
use pet_core::telemetry::TelemetryEvent;
365+
use std::sync::Mutex;
340366

341-
struct CountingReporter;
342-
impl Reporter for CountingReporter {
343-
fn report_manager(&self, _manager: &EnvManager) {}
344-
fn report_environment(&self, _env: &PythonEnvironment) {}
367+
#[derive(Default)]
368+
struct RecordingReporter {
369+
environments: Mutex<Vec<String>>,
370+
managers: Mutex<Vec<PathBuf>>,
371+
}
372+
impl Reporter for RecordingReporter {
373+
fn report_manager(&self, manager: &EnvManager) {
374+
self.managers
375+
.lock()
376+
.unwrap()
377+
.push(manager.executable.clone());
378+
}
379+
fn report_environment(&self, env: &PythonEnvironment) {
380+
self.environments
381+
.lock()
382+
.unwrap()
383+
.push(env.name.clone().unwrap_or_default());
384+
}
345385
fn report_telemetry(&self, _event: &TelemetryEvent) {}
346386
}
347387

348388
let locator = create_locator();
389+
let cached_manager = EnvManager::new(
390+
PathBuf::from("C:\\fake\\python.exe"),
391+
pet_core::manager::EnvManagerType::Conda,
392+
None,
393+
);
349394
let cached = LocatorResult {
350-
managers: vec![],
395+
managers: vec![cached_manager.clone()],
351396
environments: vec![PythonEnvironment {
352397
name: Some("cached".to_string()),
353398
..Default::default()
@@ -359,8 +404,10 @@ mod tests {
359404
.unwrap()
360405
.replace(cached.clone());
361406

362-
locator.find(&CountingReporter);
407+
let reporter = RecordingReporter::default();
408+
locator.find(&reporter);
363409

410+
// (a) The cache must still be populated and unchanged.
364411
let after = locator
365412
.search_result
366413
.lock()
@@ -373,6 +420,19 @@ mod tests {
373420
"find() must not clear the cache before populating",
374421
);
375422
assert_eq!(after.environments[0].name.as_deref(), Some("cached"));
423+
// (b) The cached entries must have been replayed to the reporter
424+
// — otherwise WindowsRegistry discoveries would silently
425+
// disappear on every refresh after the first.
426+
assert_eq!(
427+
reporter.environments.lock().unwrap().as_slice(),
428+
&["cached".to_string()],
429+
"find() must replay cached environments to the reporter on a cache hit",
430+
);
431+
assert_eq!(
432+
reporter.managers.lock().unwrap().as_slice(),
433+
&[PathBuf::from("C:\\fake\\python.exe")],
434+
"find() must replay cached managers to the reporter on a cache hit",
435+
);
376436
}
377437

378438
/// Smoke test: on a fresh locator (empty cache), `find()` runs the new

0 commit comments

Comments
 (0)