Skip to content

Report the device search into the form only once there is a window - #23

Merged
Skorinn merged 1 commit into
masterfrom
device-search-guard
Sep 9, 2026
Merged

Skorinn merged 1 commit into
masterfrom
device-search-guard

Conversation

@Skorinn

@Skorinn Skorinn commented Sep 9, 2026

Copy link
Copy Markdown
Owner

The v1.0.1 release failed a second time, on a different test in the same family. StatusBoxText_SetProperty_PropertiesCorrect asserted on the status box and found "Checking attached devices and updating port list..." in it — the device search writing to the form while the test read it.

Why the first fix could not have caught this

Clearing the parent after each test stops a search outliving the test that started it. This race is inside a single test: the form's own constructor starts the search, and the search then writes to the form the test is still using. Different shape, same root.

The actual fault

Every report the search makes decides how to marshal itself by asking the form. A form that was never shown has no window handle, and InvokeRequired on a handleless control answers false — which reads as "already on the right thread". So a pool thread writes the controls directly.

The search now asks whether there is a window before reporting at all. All five of its calls into the form already shared one guard, so this is one place:

private static bool ParentIsReady
{
    get => ((null != m_Parent) && (false == Terminating) && m_Parent.IsHandleCreated);
}

IGeneratorForm gains IsHandleCreated, which Form already supplies from Control.

The regression that guard would have caused

Worth stating plainly, because it would have been worse than the flake: the search was started from the constructor. On a quick machine it could finish before the window existed, and the new guard would then have dropped its results — leaving the port list empty until a device was plugged or unplugged, with no retry.

So the search is started from OnHandleCreated instead, the moment reporting becomes possible. It can now neither race the window nor be dropped. Tests never show a form, so they no longer start a search at all — which removes the race at its source rather than guarding against it.

Verification

  • Both configurations rebuilt clean; suite run six times over, 243/243 each. The flake never reproduced locally, so this is confidence rather than proof — CI is the real test.
  • The built application still announces the search on the status bar (Checking attached devices and updating port list...). That message goes through the same ParentIsReady guard as the device list, so seeing it proves both paths still report.
  • check-nodevice 23/23 against the built application.

The eight IGeneratorForm mocks in the device-thread tests now report a window, since they stand in for a form that has been shown.

🤖 Generated with Claude Code

https://claude.ai/code/session_0153VVkWg7DQmdaNLcvtY37w

The v1.0.1 release failed again, on a different test in the same family:
StatusBoxText_SetProperty_PropertiesCorrect asserted on the status box and
found "Checking attached devices and updating port list..." in it. That is the
device search writing to the form while the test was reading it.

The previous fix let go of the form after each test, which stops a search
outliving the test that started it. It could not help here, because this race
is inside a single test: the form's own constructor starts the search, and the
search then writes to the form the test is still using.

The guard was the real fault. Every report the search makes decides how to
marshal itself by asking the form, and a form that was never shown has no
window handle. InvokeRequired on a handleless control answers false, which
reads as "already on the right thread", so a pool thread writes the controls
directly. The search now asks whether there is a window before reporting at
all, in one place that all five of its calls into the form go through, and
IGeneratorForm carries IsHandleCreated so it can be asked.

That guard alone would have swapped one fault for a worse one. The search was
started from the constructor, so on a quick machine it could finish before the
window existed and have its results dropped, leaving the port list empty until
a device was plugged or unplugged. It is started from OnHandleCreated now,
which is the moment it becomes possible to report, so it can neither race the
window nor be dropped. Tests never show a form, so they no longer start a
search at all, which removes the race at its source rather than guarding it.

Verified by running the suite six times over, and by watching the built
application: it still announces the search on the status bar, which is the
same guard the device list goes through, so both report as before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153VVkWg7DQmdaNLcvtY37w
Copilot AI lite review requested due to automatic review settings September 9, 2026 12:41
@Skorinn
Skorinn merged commit 3efd018 into master Sep 9, 2026
@Skorinn
Skorinn deleted the device-search-guard branch September 9, 2026 12:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

OnHandleCreated can fire multiple times if the WinForms handle is recreated, so starting the device search unconditionally there risks repeated WMI searches/status churn and should be guarded to run once per form instance.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR fixes a UI-thread affinity race in the device-search background thread by ensuring the search only reports into GeneratorForm once the form has a real window handle, avoiding cross-thread control updates during unit tests and early form lifetime.

Changes:

  • Added IGeneratorForm.IsHandleCreated and used it in DeviceUpdateThread to guard all UI reporting behind “parent + not terminating + handle exists”.
  • Moved the initial device search start from the form constructor to GeneratorForm.OnHandleCreated, when reporting/marshalling becomes valid.
  • Updated DeviceUpdateThread tests to configure mocked IGeneratorForm instances as “handle created”.
File summaries
File Description
GeneratorForm.cs Adds IsHandleCreated to IGeneratorForm and starts the device search from OnHandleCreated instead of the constructor.
DeviceUpdateThread.cs Centralizes UI-reporting eligibility via ParentIsReady to prevent reporting into handleless forms.
tests/RandomNumberGenerator.Test/DeviceUpdateThread.Test.cs Updates mocks to reflect the new “form must have a handle” requirement.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread GeneratorForm.cs
Comment on lines +1356 to +1360
// The search runs on a pool thread and reports what it finds by calling this form, so it cannot
// start until there is a window for it to call. Started from the constructor it raced the window
// into being, and a search that got there first had its results dropped.
StartDeviceSearch();
}
Comment on lines +135 to +139
// Stand in for a form that has been shown. The search only reports into a form whose window
// exists, because it decides how to marshal by asking the form, and a form with no window
// cannot answer that question.
mockParent.Setup(mock => mock.IsHandleCreated).Returns(true);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants