Skip to content

[dotnet] Terminate driver process when the parent application exits - #17971

Open
ashrafiucse wants to merge 1 commit into
SeleniumHQ:trunkfrom
ashrafiucse:fix-dotnet-driver-process-leak-on-app-exit
Open

[dotnet] Terminate driver process when the parent application exits#17971
ashrafiucse wants to merge 1 commit into
SeleniumHQ:trunkfrom
ashrafiucse:fix-dotnet-driver-process-leak-on-app-exit

Conversation

@ashrafiucse

Copy link
Copy Markdown

🔗 Related Issues

Fixes #17095

🔄 Types of changes

  • Bug fix (backwards compatible)

📝 Description

Root cause. Since #16782, driver services start with CreateNoWindow = true, so the driver server executable no longer attaches to the parent application's console. On Windows, that console was what tied the driver's lifetime to the application: terminating the console (e.g. stopping a debugging session in an IDE) used to take the driver — and the browser it spawned — down with it. Now nothing does: when the application is killed abruptly, no dispose/finalizer/exit code runs at all, and the driver and browser processes are leaked.

This restores the lifetime coupling with three complementary mechanisms:

  1. Job object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE (Windows, primary fix). The driver process is placed in a job object whose handle is held by the application process. When the application exits — including when it is terminated abruptly via TerminateProcess — the kernel closes the handle and terminates every process in the job, along with their children (the browser). This is the only mechanism that works when the application is killed without any chance to run cleanup code, which is the exact scenario reported in the issue.
  2. AppDomain.CurrentDomain.ProcessExit handler. Terminates leftover driver processes when the application exits gracefully without disposing its services (e.g. Environment.Exit). Works on all platforms.
  3. Finalizer on DriverService. Best-effort synchronous termination when a service is garbage collected without being disposed, as suggested in the issue.

Notes:

  • The job object setup is best-effort: if the job cannot be created or the process cannot be assigned (e.g. restrictive nested-job environments), the failure is logged at debug level and the driver starts normally, with the exit-event tracking still in place.
  • Properly disposed services are unaffected: the graceful shutdown path is unchanged and stops the driver before anything else.
  • Non-Windows platforms get the exit-event and finalizer safety nets; there the driver already dies when the application's redirected output pipes close.

Version note. While the issue report mentions 4.36.0, the regression commit (#16782) first shipped in 4.40.0 (git: selenium-4.36.0selenium-4.39.0 do not contain it) — the reported intermediate-version testing was likely affected by cached binaries.

🧪 How has this been tested?

  • New unit test KillOnCloseJobObjectTests (Windows-only, [Platform("Win")]) verifies the actual kernel contract: a process added to the job object is terminated when the job object is disposed — no mocks, a real ping child process.
  • Existing DriverServiceTests pass unchanged.
  • All three target frameworks (net462, netstandard2.0, net8.0) build via bazel; ./scripts/format.sh --pre-commit passes.
  • Manual verification of the exact issue repro (stop a console app hosting new ChromeDriver() from an IDE before it disposes) still needs a Windows machine — the unit test covers the same contract continuously on Windows CI.

Manual repro steps (Windows):

using var chromeDriver = new ChromeDriver();
Thread.Sleep(1000000); // stop the app from the IDE while sleeping

Before this change: chromedriver.exe and chrome.exe remain running. After: both are terminated by the operating system.

📚 Notes for reviewers

  • Cross-binding impact: none by design — this is Windows process-management behavior specific to the .NET binding; other bindings manage child lifetimes differently (Java relies on JVM shutdown hooks and Unix process groups).
  • The job object is a single static instance shared by all driver processes of the application; its handle is intentionally never closed before process exit.

@qodo-code-review

Copy link
Copy Markdown
Contributor

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@selenium-ci selenium-ci added the C-dotnet .NET Bindings label Aug 29, 2026
@cgoldberg
cgoldberg requested a review from nvborisenko August 30, 2026 17:13
Comment thread dotnet/src/webdriver/DriverService.cs Outdated

// Driver services that are still running. Used to terminate leftover driver
// processes when the application exits without disposing them.
private static readonly ConcurrentDictionary<DriverService, byte> ActiveServices = new();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Instead of one global shared state I propose to create dedicated "KillOnCloseJobObject" per DriverService. If it is still performant.

Comment thread dotnet/src/webdriver/DriverService.cs Outdated
Comment on lines +431 to +436
// A job object terminated on close makes the operating system kill
// the driver (and browser) processes even when this process is killed
// abruptly, e.g. stopping a debugging session in an IDE, when no exit
// event is raised and no cleanup code can run. Failure to set this up
// must not prevent the driver from starting: the lifetime tracking via
// the exit event above remains in place either way.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I remember user is able to start driver service process and say like "keep browser running even if parent process exits". Is it still possible?

Comment thread dotnet/src/webdriver/DriverService.cs Outdated
{
if (_logger.IsEnabled(LogEventLevel.Debug))
{
_logger.Debug($"Unable to track the driver process in a job object: {ex.Message}");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So low-level, let's move it to Trace level.

Comment thread dotnet/src/webdriver/DriverService.cs Outdated
Comment thread dotnet/src/webdriver/DriverService.cs Outdated
Comment on lines +213 to +214
/// Finalizes this instance, terminating the driver process as a best effort
/// when the user forgot to dispose of this <see cref="DriverService"/>.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Be more generic plz here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Restores .NET driver and browser process cleanup when the parent application exits.

Changes:

  • Adds Windows kill-on-close job-object management.
  • Adds process-exit tracking and finalizer cleanup.
  • Adds a Windows job-object integration test.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
dotnet/src/webdriver/DriverService.cs Integrates process lifetime tracking and cleanup.
dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs Implements Windows job-object handling.
dotnet/test/webdriver/KillOnCloseJobObjectTests.cs Verifies kill-on-close behavior.

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

Comment thread dotnet/src/webdriver/DriverService.cs Outdated

// Driver services that are still running. Used to terminate leftover driver
// processes when the application exits without disposing them.
private static readonly ConcurrentDictionary<DriverService, byte> ActiveServices = new();
Comment on lines +45 to +46
/// This class is a no-op safety net on non-Windows platforms and must only be
/// used there; callers are responsible for platform checks.
@nvborisenko

Copy link
Copy Markdown
Member

In general I support the fix for Windows. Let's find hidden stones. Please address feedback and we will look closer.

@ashrafiucse
ashrafiucse force-pushed the fix-dotnet-driver-process-leak-on-app-exit branch from d87e0f5 to f8aed74 Compare September 1, 2026 05:53
@ashrafiucse

Copy link
Copy Markdown
Author

Thanks @nvborisenko and @Copilot for the review! All feedback is addressed in the updated commit.

1. Dedicated KillOnCloseJobObject per DriverService (no global shared state) — All static state is gone (ActiveServices, the shared job object, the ProcessExit handler registration). Each service now creates its own job object in StartAsync and disposes it when it stops, so stopping one service can never affect another one's processes. The exit story is also simpler: the OS terminates the job members when the last job handle closes at process exit, so no managed exit handler is needed anymore (the previous ProcessExit handler killed processes just as abruptly as the job object does). Creating a job object costs three kernel calls per service start, so performance is unaffected.

A side benefit: closing the job handle when the service stops now also cleans up a browser left behind by a driver that was terminated without ending its session first.

2. "Keep browser running even if parent process exits" — is it still possible? — There has never been an explicit API for this in the .NET bindings. Before 4.36 the driver inherited the parent's console and died with it (exactly the behavior #17095 asks to restore); since #16782 it silently survives the parent, which is the reported leak. With this change the coupling is restored on Windows by default. The supported way to keep a driver alive beyond the application remains starting it out-of-process and attaching with new RemoteWebDriver(url, options) — no DriverService is involved, so nothing is tracked and nothing is killed. If you think an in-process opt-out is warranted, I'm happy to add a property for it — please advise.

3. Copilot: the finalizer never ran because ActiveServices strongly rooted every service — Fixed by design: with the registry removed there is no rooting anymore, so an abandoned running service is collectable and ~DriverService() terminates its driver process. The abandoned-instance path is now covered by a GC regression test (ProcessAddedToJobObjectIsTerminatedWhenJobObjectIsGarbageCollected). A full end-to-end GC test at the DriverService level isn't feasible in the unit suite, since StartAsync requires a live driver HTTP endpoint to initialize.

4. Low-level logging → Trace — All job-object messages now log at Trace level.

5. Finalizer doc — Reworded generically.

6. KillOnCloseJobObject docs — Corrected the contradictory remark: the class is implemented on top of Windows-only APIs and throws on other platforms; callers must check the platform before creating an instance.

Verified on Linux (where job tracking is skipped and behavior is unchanged): bazel test //dotnet/test/webdriver:webdriver produces the same results as the trunk baseline in this environment (only the pre-existing browser-dependent tests fail here; no new failures). The Windows job-object tests are exercised by CI.

@qodo-code-review

qodo-code-review Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (4) 📜 Skill insights (0)

Grey Divider


Action required

1. Job query failure is silent 📘 Rule violation ◔ Observability
Description
When QueryInformationJobObject fails, TryDisposeIfEmpty silently returns false, leaving the
job handle retained without any diagnostic explaining why cleanup failed. This makes
process-lifetime tracking and native-handle retention failures opaque to users troubleshooting the
new behavior.
Code

dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[R144-147]

+        if (!QueryInformationJobObject(this.jobHandle, JobObjectBasicAccountingInformationClass, out JOBOBJECT_BASIC_ACCOUNTING_INFORMATION accounting, (uint)Marshal.SizeOf<JOBOBJECT_BASIC_ACCOUNTING_INFORMATION>(), IntPtr.Zero))
+        {
+            return false;
+        }
Evidence
Compliance rule 6 requires user-relevant failures in changed operational paths to include
appropriate diagnostics. The cited branch handles a failed Windows job-object query only by
returning false, unlike the surrounding job creation and assignment failure paths that emit trace
logging.

AGENTS.md: Add User-Relevant Diagnostic Logging: AGENTS.md: Add User-Relevant Diagnostic Logging
dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[144-147]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`TryDisposeIfEmpty` silently returns when `QueryInformationJobObject` fails, so users cannot diagnose why a job handle was not released.
## Issue Context
Capture the Win32 error immediately after the failed native call and log it at the repository-appropriate diagnostic level before returning `false`.
## Fix Focus Areas
- dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[144-147]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Job handles accumulate indefinitely 📘 Rule violation ☼ Reliability
Description
Every Windows driver start adds a strongly rooted KillOnCloseJobObject to the static
DriverProcessJobs dictionary, while shutdown only clears the instance field and never removes or
disposes the entry, permanently retaining a managed object, native handle, and kernel job object
even after all job processes exit. Long-running applications that repeatedly create or restart
driver sessions can therefore exhaust process handles and unexpectedly break existing driver-start
behavior.
Code

dotnet/src/webdriver/DriverService.cs[R40-42]

+    // Entries are never removed — the set holds one small handle per started
+    // service and the operating system releases all of them at process exit.
+    private static readonly ConcurrentDictionary<KillOnCloseJobObject, byte> DriverProcessJobs = new();
Evidence
Compliance rule 1 requires default public behavior to remain usable after upgrade. The static
dictionary is explicitly documented as never removing job objects, every Windows service start
creates and inserts a new job, shutdown only nulls the per-instance reference, and each retained
KillOnCloseJobObject owns a SafeFileHandle whose native handle is released only by Dispose,
proving unbounded native-handle retention that can eventually prevent further driver creation.

AGENTS.md: Preserve Public API and ABI Compatibility and Follow Deprecation Policy: AGENTS.md: Preserve Public API and ABI Compatibility and Follow Deprecation Policy: AGENTS.md: Preserve Public API and ABI Compatibility and Follow Deprecation Policy: AGENTS.md: Preserve Public API and ABI Compatibility and Follow Deprecation Policy
dotnet/src/webdriver/DriverService.cs[35-42]
dotnet/src/webdriver/DriverService.cs[415-440]
dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[56-63]
dotnet/src/webdriver/DriverService.cs[35-46]
dotnet/src/webdriver/DriverService.cs[450-493]
dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[63-86]
dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[123-130]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The static `DriverProcessJobs` collection permanently accumulates a `KillOnCloseJobObject`, native handle, and kernel job object for every Windows driver start because shutdown paths never remove or dispose dictionary entries. This causes unbounded handle growth in applications that repeatedly create or restart driver sessions.
## Issue Context
A job must remain open while it still contains a detached browser, including when preserving `ChromiumOptions.LeaveBrowserRunning`, but retaining it after the driver exits and the job has no active processes no longer protects anything. Query the job's active-process count after shutdown, remove and dispose empty jobs, preserve jobs containing processes intended to outlive the service, and clean up newly created jobs that never successfully track a process.
## Fix Focus Areas
- dotnet/src/webdriver/DriverService.cs[35-46]
- dotnet/src/webdriver/DriverService.cs[415-449]
- dotnet/src/webdriver/DriverService.cs[450-493]
- dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[56-145]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. LeaveBrowserRunning browsers are killed 📘 Rule violation ≡ Correctness
Description
Normal DriverService shutdown now closes the kill-on-close job, terminating Chromium descendants
even when the public LeaveBrowserRunning (detach) option promises the browser will remain
running. This incompatibly changes existing public functionality without deprecation or an
alternative.
Code

dotnet/src/webdriver/DriverService.cs[476]

+            this.DisposeDriverProcessJob();
Evidence
Rule 1 requires existing public functionality to remain compatible. LeaveBrowserRunning documents
that Chromium remains alive after ChromeDriver exits, while the new normal shutdown path closes a
job explicitly configured to kill the driver and its browser descendants.

AGENTS.md: Preserve Public API and ABI Compatibility Through Deprecation: AGENTS.md: Preserve Public API and ABI Compatibility Through Deprecation: AGENTS.md: Preserve Public API and ABI Compatibility Through Deprecation: AGENTS.md: Preserve Public API and ABI Compatibility Through Deprecation: AGENTS.md: Preserve Public API and ABI Compatibility Through Deprecation: AGENTS.md: Preserve Public API and ABI Compatibility Through Deprecation: AGENTS.md: Preserve Public API and ABI Compatibility Through Deprecation: AGENTS.md: Preserve Public API and ABI Compatibility Through Deprecation
dotnet/src/webdriver/Chromium/ChromiumOptions.cs[97-100]
dotnet/src/webdriver/Chromium/ChromiumOptions.cs[544-547]
dotnet/src/webdriver/DriverService.cs[411-421]
dotnet/src/webdriver/DriverService.cs[474-479]
dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[114-121]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
On Windows, normal driver disposal closes a kill-on-close job containing the driver and its browser descendants, which breaks the public `LeaveBrowserRunning` behavior.
## Issue Context
`ChromiumOptions.LeaveBrowserRunning` is documented to keep Chromium running after ChromeDriver exits and is serialized as `detach: true`. Preserve that contract while retaining abrupt-parent-exit cleanup for ordinary sessions.
## Fix Focus Areas
- dotnet/src/webdriver/DriverService.cs[404-438]
- dotnet/src/webdriver/DriverService.cs[441-479]
- dotnet/src/webdriver/Chromium/ChromiumOptions.cs[97-100]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View action required (2)
4. Tests bypass DriverService contract 📘 Rule violation ☼ Reliability
Description
The new tests exercise KillOnCloseJobObject directly, but never cover DriverService
startup/disposal or the supported detached-browser path affected by the integration. Consequently,
they cannot detect the introduced LeaveBrowserRunning regression.
Code

dotnet/test/webdriver/KillOnCloseJobObjectTests.cs[R42-45]

+            using (KillOnCloseJobObject jobObject = new())
+            {
+                jobObject.AddProcess(process);
+            }
Evidence
Rule 5 requires regression coverage that exercises realistic production contracts. The tests call
the helper directly, whereas production behavior is introduced by
DriverService.TrackDriverProcessLifetime; the public detached-browser contract is documented
elsewhere and receives no coverage in these tests.

AGENTS.md: Prefer Small Tests and Avoid Contract-Distorting Mocks: AGENTS.md: Prefer Small Tests and Avoid Contract-Distorting Mocks: AGENTS.md: Prefer Small Tests and Avoid Contract-Distorting Mocks: AGENTS.md: Prefer Small Tests and Avoid Contract-Distorting Mocks: AGENTS.md: Prefer Small Tests and Avoid Contract-Distorting Mocks: AGENTS.md: Prefer Small Tests and Avoid Contract-Distorting Mocks: AGENTS.md: Prefer Small Tests and Avoid Contract-Distorting Mocks: AGENTS.md: Prefer Small Tests and Avoid Contract-Distorting Mocks
dotnet/test/webdriver/KillOnCloseJobObjectTests.cs[28-45]
dotnet/test/webdriver/KillOnCloseJobObjectTests.cs[71-89]
dotnet/src/webdriver/DriverService.cs[281-284]
dotnet/src/webdriver/Chromium/ChromiumOptions.cs[97-100]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The regression tests validate only the job-object primitive and do not exercise the production `DriverService` integration or preserve detached-browser behavior.
## Issue Context
Add focused Windows coverage through the actual service lifecycle, including graceful disposal and a Chromium `LeaveBrowserRunning` case. Keep the lower-level kernel-contract test if useful, but do not rely on it as the sole behavioral coverage.
## Fix Focus Areas
- dotnet/test/webdriver/KillOnCloseJobObjectTests.cs[28-125]
- dotnet/test/webdriver/DriverServiceTests.cs[30-60]
- dotnet/src/webdriver/DriverService.cs[281-284]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Assignment failure loses tracking 🐞 Bug ☼ Reliability
Description
When AssignProcessToJobObject fails, AddProcess only logs and returns, while DriverService
installs no AppDomain.ProcessExit handler or other fallback tracking. In nested-job or
already-exited assignment cases, an application exit without explicit service disposal can therefore
still leave the driver and browser running—the leak this fallback is meant to prevent.
Code

dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[R93-98]

+            if (!AssignProcessToJobObject(this.jobHandle, process.Handle))
+            {
+                if (_logger.IsEnabled(LogEventLevel.Trace))
+                {
+                    _logger.Trace($"Unable to add process {process.Id} to the driver job object: {new Win32Exception().Message}");
+                }
Evidence
The assignment failure path only emits a trace and then returns. The sole integration method creates
a job and calls AddProcess, while the only added non-disposal cleanup is an instance finalizer; no
process-exit registration exists in the WebDriver source.

dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[89-112]
dotnet/src/webdriver/DriverService.cs[404-430]
dotnet/src/webdriver/DriverService.cs[204-210]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A failed job-object assignment is swallowed after logging, leaving the driver process without the promised application-exit cleanup fallback. If the application exits without disposing the service, the driver and browser can remain alive.
## Issue Context
`DriverService.TrackDriverProcessLifetime` only creates the Windows job and calls `AddProcess`; there is no `AppDomain.CurrentDomain.ProcessExit` subscription or process registry. The finalizer is only best-effort garbage-collection cleanup and does not replace explicit process-exit tracking.
## Fix Focus Areas
- dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[89-112]
- dotnet/src/webdriver/DriverService.cs[404-429]
- dotnet/src/webdriver/DriverService.cs[204-210]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

6. Failed configuration delays handle cleanup 🐞 Bug ☼ Reliability
Description
If SetInformationJobObject fails after CreateJobObject succeeds, the constructor throws without
disposing the newly owned SafeFileHandle. Repeated configuration failures can retain native job
handles until GC finalizes the safe handles, despite the caller catching the exception and
continuing normally.
Code

dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[R75-78]

+        if (!SetInformationJobObject(this.jobHandle, JobObjectExtendedLimitInformationClass, ref information, (uint)Marshal.SizeOf<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>()))
+        {
+            throw new Win32Exception("Unable to configure the job object to terminate processes on close");
+        }
Evidence
The constructor stores a valid owning handle, then throws on configuration failure without calling
Dispose; the only explicit handle cleanup is the instance Dispose method, and the integration
catches this constructor exception.

dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[64-78]
dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[114-121]
dotnet/src/webdriver/DriverService.cs[418-429]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The constructor does not deterministically release the job handle when job configuration fails. This can accumulate native handles until garbage collection under repeated failures.
## Issue Context
`CreateJobObject` has already returned an owning `SafeFileHandle` before `SetInformationJobObject` runs. Since construction throws, callers cannot invoke `KillOnCloseJobObject.Dispose`, and `DriverService` intentionally catches the exception and continues.
## Fix Focus Areas
- dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[64-78]
- dotnet/src/webdriver/DriverService.cs[418-429]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: ⚖️ Balanced

Grey Divider

Tip of the day
💡 Did you know, you can describe a rule in plain language on the Rules page and Qodo drafts it for you

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Previous reviews

Review updated until commit 21d3641 ⚖️ Balanced

Results up to commit f8aed74 ⚖️ Balanced


🐞 Bugs (2) 📘 Rule violations (2) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Action required
1. LeaveBrowserRunning browsers are killed 📘 Rule violation ≡ Correctness
Description
Normal DriverService shutdown now closes the kill-on-close job, terminating Chromium descendants
even when the public LeaveBrowserRunning (detach) option promises the browser will remain
running. This incompatibly changes existing public functionality without deprecation or an
alternative.
Code

dotnet/src/webdriver/DriverService.cs[476]

+            this.DisposeDriverProcessJob();
Evidence
Rule 1 requires existing public functionality to remain compatible. LeaveBrowserRunning documents
that Chromium remains alive after ChromeDriver exits, while the new normal shutdown path closes a
job explicitly configured to kill the driver and its browser descendants.

AGENTS.md: Preserve Public API and ABI Compatibility Through Deprecation
dotnet/src/webdriver/Chromium/ChromiumOptions.cs[97-100]
dotnet/src/webdriver/Chromium/ChromiumOptions.cs[544-547]
dotnet/src/webdriver/DriverService.cs[411-421]
dotnet/src/webdriver/DriverService.cs[474-479]
dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[114-121]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
On Windows, normal driver disposal closes a kill-on-close job containing the driver and its browser descendants, which breaks the public `LeaveBrowserRunning` behavior.

## Issue Context
`ChromiumOptions.LeaveBrowserRunning` is documented to keep Chromium running after ChromeDriver exits and is serialized as `detach: true`. Preserve that contract while retaining abrupt-parent-exit cleanup for ordinary sessions.

## Fix Focus Areas
- dotnet/src/webdriver/DriverService.cs[404-438]
- dotnet/src/webdriver/DriverService.cs[441-479]
- dotnet/src/webdriver/Chromium/ChromiumOptions.cs[97-100]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Tests bypass DriverService contract 📘 Rule violation ☼ Reliability
Description
The new tests exercise KillOnCloseJobObject directly, but never cover DriverService
startup/disposal or the supported detached-browser path affected by the integration. Consequently,
they cannot detect the introduced LeaveBrowserRunning regression.
Code

dotnet/test/webdriver/KillOnCloseJobObjectTests.cs[R42-45]

+            using (KillOnCloseJobObject jobObject = new())
+            {
+                jobObject.AddProcess(process);
+            }
Evidence
Rule 5 requires regression coverage that exercises realistic production contracts. The tests call
the helper directly, whereas production behavior is introduced by
DriverService.TrackDriverProcessLifetime; the public detached-browser contract is documented
elsewhere and receives no coverage in these tests.

AGENTS.md: Prefer Small Tests and Avoid Contract-Distorting Mocks
dotnet/test/webdriver/KillOnCloseJobObjectTests.cs[28-45]
dotnet/test/webdriver/KillOnCloseJobObjectTests.cs[71-89]
dotnet/src/webdriver/DriverService.cs[281-284]
dotnet/src/webdriver/Chromium/ChromiumOptions.cs[97-100]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The regression tests validate only the job-object primitive and do not exercise the production `DriverService` integration or preserve detached-browser behavior.

## Issue Context
Add focused Windows coverage through the actual service lifecycle, including graceful disposal and a Chromium `LeaveBrowserRunning` case. Keep the lower-level kernel-contract test if useful, but do not rely on it as the sole behavioral coverage.

## Fix Focus Areas
- dotnet/test/webdriver/KillOnCloseJobObjectTests.cs[28-125]
- dotnet/test/webdriver/DriverServiceTests.cs[30-60]
- dotnet/src/webdriver/DriverService.cs[281-284]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Assignment failure loses tracking 🐞 Bug ☼ Reliability
Description
When AssignProcessToJobObject fails, AddProcess only logs and returns, while DriverService
installs no AppDomain.ProcessExit handler or other fallback tracking. In nested-job or
already-exited assignment cases, an application exit without explicit service disposal can therefore
still leave the driver and browser running—the leak this fallback is meant to prevent.
Code

dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[R93-98]

+            if (!AssignProcessToJobObject(this.jobHandle, process.Handle))
+            {
+                if (_logger.IsEnabled(LogEventLevel.Trace))
+                {
+                    _logger.Trace($"Unable to add process {process.Id} to the driver job object: {new Win32Exception().Message}");
+                }
Evidence
The assignment failure path only emits a trace and then returns. The sole integration method creates
a job and calls AddProcess, while the only added non-disposal cleanup is an instance finalizer; no
process-exit registration exists in the WebDriver source.

dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[89-112]
dotnet/src/webdriver/DriverService.cs[404-430]
dotnet/src/webdriver/DriverService.cs[204-210]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A failed job-object assignment is swallowed after logging, leaving the driver process without the promised application-exit cleanup fallback. If the application exits without disposing the service, the driver and browser can remain alive.

## Issue Context
`DriverService.TrackDriverProcessLifetime` only creates the Windows job and calls `AddProcess`; there is no `AppDomain.CurrentDomain.ProcessExit` subscription or process registry. The finalizer is only best-effort garbage-collection cleanup and does not replace explicit process-exit tracking.

## Fix Focus Areas
- dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[89-112]
- dotnet/src/webdriver/DriverService.cs[404-429]
- dotnet/src/webdriver/DriverService.cs[204-210]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
4. Failed configuration delays handle cleanup 🐞 Bug ☼ Reliability
Description
If SetInformationJobObject fails after CreateJobObject succeeds, the constructor throws without
disposing the newly owned SafeFileHandle. Repeated configuration failures can retain native job
handles until GC finalizes the safe handles, despite the caller catching the exception and
continuing normally.
Code

dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[R75-78]

+        if (!SetInformationJobObject(this.jobHandle, JobObjectExtendedLimitInformationClass, ref information, (uint)Marshal.SizeOf<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>()))
+        {
+            throw new Win32Exception("Unable to configure the job object to terminate processes on close");
+        }
Evidence
The constructor stores a valid owning handle, then throws on configuration failure without calling
Dispose; the only explicit handle cleanup is the instance Dispose method, and the integration
catches this constructor exception.

dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[64-78]
dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[114-121]
dotnet/src/webdriver/DriverService.cs[418-429]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The constructor does not deterministically release the job handle when job configuration fails. This can accumulate native handles until garbage collection under repeated failures.

## Issue Context
`CreateJobObject` has already returned an owning `SafeFileHandle` before `SetInformationJobObject` runs. Since construction throws, callers cannot invoke `KillOnCloseJobObject.Dispose`, and `DriverService` intentionally catches the exception and continues.

## Fix Focus Areas
- dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[64-78]
- dotnet/src/webdriver/DriverService.cs[418-429]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit bd3cf69 ⚖️ Balanced


🐞 Bugs (0) 📘 Rule violations (1) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Action required
1. Job handles accumulate indefinitely 📘 Rule violation ☼ Reliability
Description
Every Windows driver start adds a strongly rooted KillOnCloseJobObject to the static
DriverProcessJobs dictionary, while shutdown only clears the instance field and never removes or
disposes the entry, permanently retaining a managed object, native handle, and kernel job object
even after all job processes exit. Long-running applications that repeatedly create or restart
driver sessions can therefore exhaust process handles and unexpectedly break existing driver-start
behavior.
Code

dotnet/src/webdriver/DriverService.cs[R40-42]

+    // Entries are never removed — the set holds one small handle per started
+    // service and the operating system releases all of them at process exit.
+    private static readonly ConcurrentDictionary<KillOnCloseJobObject, byte> DriverProcessJobs = new();
Evidence
Compliance rule 1 requires default public behavior to remain usable after upgrade. The static
dictionary is explicitly documented as never removing job objects, every Windows service start
creates and inserts a new job, shutdown only nulls the per-instance reference, and each retained
KillOnCloseJobObject owns a SafeFileHandle whose native handle is released only by Dispose,
proving unbounded native-handle retention that can eventually prevent further driver creation.

AGENTS.md: Preserve Public API and ABI Compatibility and Follow Deprecation Policy
dotnet/src/webdriver/DriverService.cs[35-42]
dotnet/src/webdriver/DriverService.cs[415-440]
dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[56-63]
dotnet/src/webdriver/DriverService.cs[35-46]
dotnet/src/webdriver/DriverService.cs[450-493]
dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[63-86]
dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[123-130]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The static `DriverProcessJobs` collection permanently accumulates a `KillOnCloseJobObject`, native handle, and kernel job object for every Windows driver start because shutdown paths never remove or dispose dictionary entries. This causes unbounded handle growth in applications that repeatedly create or restart driver sessions.

## Issue Context
A job must remain open while it still contains a detached browser, including when preserving `ChromiumOptions.LeaveBrowserRunning`, but retaining it after the driver exits and the job has no active processes no longer protects anything. Query the job's active-process count after shutdown, remove and dispose empty jobs, preserve jobs containing processes intended to outlive the service, and clean up newly created jobs that never successfully track a process.

## Fix Focus Areas
- dotnet/src/webdriver/DriverService.cs[35-46]
- dotnet/src/webdriver/DriverService.cs[415-449]
- dotnet/src/webdriver/DriverService.cs[450-493]
- dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[56-145]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit 803a9a7 ⚖️ Balanced


🐞 Bugs (0) 📘 Rule violations (1) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Action required
1. Job query failure is silent 📘 Rule violation ◔ Observability
Description
When QueryInformationJobObject fails, TryDisposeIfEmpty silently returns false, leaving the
job handle retained without any diagnostic explaining why cleanup failed. This makes
process-lifetime tracking and native-handle retention failures opaque to users troubleshooting the
new behavior.
Code

dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[R144-147]

+        if (!QueryInformationJobObject(this.jobHandle, JobObjectBasicAccountingInformationClass, out JOBOBJECT_BASIC_ACCOUNTING_INFORMATION accounting, (uint)Marshal.SizeOf<JOBOBJECT_BASIC_ACCOUNTING_INFORMATION>(), IntPtr.Zero))
+        {
+            return false;
+        }
Evidence
Compliance rule 6 requires user-relevant failures in changed operational paths to include
appropriate diagnostics. The cited branch handles a failed Windows job-object query only by
returning false, unlike the surrounding job creation and assignment failure paths that emit trace
logging.

AGENTS.md: Add User-Relevant Diagnostic Logging
dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[144-147]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`TryDisposeIfEmpty` silently returns when `QueryInformationJobObject` fails, so users cannot diagnose why a job handle was not released.

## Issue Context
Capture the Win32 error immediately after the failed native call and log it at the repository-appropriate diagnostic level before returning `false`.

## Fix Focus Areas
- dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[144-147]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread dotnet/src/webdriver/DriverService.cs Outdated
}
finally
{
this.DisposeDriverProcessJob();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

1. leavebrowserrunning browsers are killed 📘 Rule violation ≡ Correctness

Normal DriverService shutdown now closes the kill-on-close job, terminating Chromium descendants
even when the public LeaveBrowserRunning (detach) option promises the browser will remain
running. This incompatibly changes existing public functionality without deprecation or an
alternative.
Agent Prompt
## Issue description
On Windows, normal driver disposal closes a kill-on-close job containing the driver and its browser descendants, which breaks the public `LeaveBrowserRunning` behavior.

## Issue Context
`ChromiumOptions.LeaveBrowserRunning` is documented to keep Chromium running after ChromeDriver exits and is serialized as `detach: true`. Preserve that contract while retaining abrupt-parent-exit cleanup for ordinary sessions.

## Fix Focus Areas
- dotnet/src/webdriver/DriverService.cs[404-438]
- dotnet/src/webdriver/DriverService.cs[441-479]
- dotnet/src/webdriver/Chromium/ChromiumOptions.cs[97-100]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +42 to +45
using (KillOnCloseJobObject jobObject = new())
{
jobObject.AddProcess(process);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

2. Tests bypass driverservice contract 📘 Rule violation ☼ Reliability

The new tests exercise KillOnCloseJobObject directly, but never cover DriverService
startup/disposal or the supported detached-browser path affected by the integration. Consequently,
they cannot detect the introduced LeaveBrowserRunning regression.
Agent Prompt
## Issue description
The regression tests validate only the job-object primitive and do not exercise the production `DriverService` integration or preserve detached-browser behavior.

## Issue Context
Add focused Windows coverage through the actual service lifecycle, including graceful disposal and a Chromium `LeaveBrowserRunning` case. Keep the lower-level kernel-contract test if useful, but do not rely on it as the sole behavioral coverage.

## Fix Focus Areas
- dotnet/test/webdriver/KillOnCloseJobObjectTests.cs[28-125]
- dotnet/test/webdriver/DriverServiceTests.cs[30-60]
- dotnet/src/webdriver/DriverService.cs[281-284]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +93 to +98
if (!AssignProcessToJobObject(this.jobHandle, process.Handle))
{
if (_logger.IsEnabled(LogEventLevel.Trace))
{
_logger.Trace($"Unable to add process {process.Id} to the driver job object: {new Win32Exception().Message}");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

3. Assignment failure loses tracking 🐞 Bug ☼ Reliability

When AssignProcessToJobObject fails, AddProcess only logs and returns, while DriverService
installs no AppDomain.ProcessExit handler or other fallback tracking. In nested-job or
already-exited assignment cases, an application exit without explicit service disposal can therefore
still leave the driver and browser running—the leak this fallback is meant to prevent.
Agent Prompt
## Issue description
A failed job-object assignment is swallowed after logging, leaving the driver process without the promised application-exit cleanup fallback. If the application exits without disposing the service, the driver and browser can remain alive.

## Issue Context
`DriverService.TrackDriverProcessLifetime` only creates the Windows job and calls `AddProcess`; there is no `AppDomain.CurrentDomain.ProcessExit` subscription or process registry. The finalizer is only best-effort garbage-collection cleanup and does not replace explicit process-exit tracking.

## Fix Focus Areas
- dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[89-112]
- dotnet/src/webdriver/DriverService.cs[404-429]
- dotnet/src/webdriver/DriverService.cs[204-210]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +75 to +78
if (!SetInformationJobObject(this.jobHandle, JobObjectExtendedLimitInformationClass, ref information, (uint)Marshal.SizeOf<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>()))
{
throw new Win32Exception("Unable to configure the job object to terminate processes on close");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Remediation recommended

4. Failed configuration delays handle cleanup 🐞 Bug ☼ Reliability

If SetInformationJobObject fails after CreateJobObject succeeds, the constructor throws without
disposing the newly owned SafeFileHandle. Repeated configuration failures can retain native job
handles until GC finalizes the safe handles, despite the caller catching the exception and
continuing normally.
Agent Prompt
## Issue description
The constructor does not deterministically release the job handle when job configuration fails. This can accumulate native handles until garbage collection under repeated failures.

## Issue Context
`CreateJobObject` has already returned an owning `SafeFileHandle` before `SetInformationJobObject` runs. Since construction throws, callers cannot invoke `KillOnCloseJobObject.Dispose`, and `DriverService` intentionally catches the exception and continues.

## Fix Focus Areas
- dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[64-78]
- dotnet/src/webdriver/DriverService.cs[418-429]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@ashrafiucse
ashrafiucse force-pushed the fix-dotnet-driver-process-leak-on-app-exit branch from f8aed74 to bd3cf69 Compare September 1, 2026 06:27
@ashrafiucse

Copy link
Copy Markdown
Author

Thanks for the follow-up review — findings #1 and #4 were valid and are fixed in the updated commit; #2 and #3 are addressed as explained below.

#1 LeaveBrowserRunning browsers are killed — confirmed and fixed. I verified in the ChromeDriver sources that on Windows the browser is not launched with CREATE_BREAKAWAY_FROM_JOB (chrome_launcher.cc only sets new_process_group for detach on non-Windows), so a browser spawned by the driver inherits our job membership, and session.cc deliberately leaves it alive when detach is set. Closing the job on service disposal (the "cleanup bonus" from my previous update) would therefore terminate a detached browser. That behavior is reverted: the job handle is now never closed by DriverService — it is kept open (rooted in a small static set of job handles, not of DriverService instances, so the finalizer-reachability fix from the previous round is unaffected) and released by the operating system when the process exits. Consequences:

  • Detached browser survives driver/service disposal and stays alive for as long as the application runs, matching the documented contract ("left running after the ChromeDriver instance is exited").
  • One deliberate tradeoff remains and directly answers @nvborisenko's earlier question: when the application itself exits, the job closes and a detached browser dies with it. Full survival across app exit would require the driver to break the browser out of the job, which ChromeDriver does not do on Windows. I think this matches the intent of [🐛 Bug]: [dotnet] The browser process doesn't close after program is stopped #17095 (the coupling to restore is "processes of this application die with it"); if a stronger guarantee for detach is wanted, that needs upstream driver cooperation or an opt-out property — happy to discuss.

#4 constructor leaks the job handle when SetInformationJobObject fails — fixed. The constructor now disposes the handle it already owns before throwing.

#3 assignment failure loses tracking — accepted as best-effort. AssignProcessToJobObject fails only in setups the job model cannot cover anyway (e.g. hosts whose own job forbids nesting on pre-Win8). The failure is logged at Trace, the driver still runs, the finalizer still cleans up abandoned instances, and normal disposal still stops the driver. Re-adding a ProcessExit registry as a partial fallback (it cannot cover abrupt exit, which is the point of the job object) would reintroduce the global service state that was asked to be removed, so I left it out.

#2 tests bypass the DriverService contract — a DriverService-level integration test is not feasible in the unit suite: StartAsync blocks until a live driver HTTP endpoint answers, and the LeaveBrowserRunning scenario additionally requires real Chrome + ChromeDriver on Windows. The job-object contract tests cover the primitive this change relies on (kill-on-close at dispose and at finalization); the Windows CI matrix exercises the real lifecycle. I can add a browser-level LeaveBrowserRunning test to the browser test suite as a follow-up if the maintainers want it.

No behavior change on Linux/macOS (job tracking is skipped there); bazel test //dotnet/test/webdriver:webdriver matches the trunk baseline in this environment (same pre-existing browser-dependent failures, no new ones).

Comment thread dotnet/src/webdriver/DriverService.cs Outdated
Comment on lines +40 to +42
// Entries are never removed — the set holds one small handle per started
// service and the operating system releases all of them at process exit.
private static readonly ConcurrentDictionary<KillOnCloseJobObject, byte> DriverProcessJobs = new();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

1. Job handles accumulate indefinitely 📘 Rule violation ☼ Reliability

Every Windows driver start adds a strongly rooted KillOnCloseJobObject to the static
DriverProcessJobs dictionary, while shutdown only clears the instance field and never removes or
disposes the entry, permanently retaining a managed object, native handle, and kernel job object
even after all job processes exit. Long-running applications that repeatedly create or restart
driver sessions can therefore exhaust process handles and unexpectedly break existing driver-start
behavior.
Agent Prompt
## Issue description
The static `DriverProcessJobs` collection permanently accumulates a `KillOnCloseJobObject`, native handle, and kernel job object for every Windows driver start because shutdown paths never remove or dispose dictionary entries. This causes unbounded handle growth in applications that repeatedly create or restart driver sessions.

## Issue Context
A job must remain open while it still contains a detached browser, including when preserving `ChromiumOptions.LeaveBrowserRunning`, but retaining it after the driver exits and the job has no active processes no longer protects anything. Query the job's active-process count after shutdown, remove and dispose empty jobs, preserve jobs containing processes intended to outlive the service, and clean up newly created jobs that never successfully track a process.

## Fix Focus Areas
- dotnet/src/webdriver/DriverService.cs[35-46]
- dotnet/src/webdriver/DriverService.cs[415-449]
- dotnet/src/webdriver/DriverService.cs[450-493]
- dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[56-145]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit bd3cf69

@ashrafiucse
ashrafiucse force-pushed the fix-dotnet-driver-process-leak-on-app-exit branch from bd3cf69 to 803a9a7 Compare September 1, 2026 06:45
@ashrafiucse

Copy link
Copy Markdown
Author

Valid point — fixed in the updated commit. The static registry no longer grows unboundedly: KillOnCloseJobObject.TryDisposeIfEmpty() (new) queries the job's active-process count via QueryInformationJobObject(JobObjectBasicAccountingInformation) and releases the handle only when no associated process is alive. DriverService calls it when the service stops:

  • Empty job (the normal case — session quit killed the browser, or no browser was ever opened): the entry is removed and the handle released immediately, so repeatedly starting/stopping services in a long-running application no longer accumulates job objects.
  • Non-empty job (e.g. a browser the user asked to keep running via LeaveBrowserRunning, or one orphaned by an abrupt driver death): the entry is intentionally retained and the handle is released by the operating system at process exit — releasing it earlier would terminate those processes, which was exactly the regression fixed in the previous round.

So accumulation is now bounded by the number of live tracked processes, not by the number of started services. Covered by a new Windows test (JobObjectIsOnlyReleasedWhenItsTrackedProcessesHaveExited). The check-then-close is race-free here: only the owner holds the job handle, so with zero members no new process can join the job between the query and the close.

Still no behavior change on Linux/macOS; bazel test //dotnet/test/webdriver:webdriver matches the trunk baseline in this environment.

Comment on lines +144 to +147
if (!QueryInformationJobObject(this.jobHandle, JobObjectBasicAccountingInformationClass, out JOBOBJECT_BASIC_ACCOUNTING_INFORMATION accounting, (uint)Marshal.SizeOf<JOBOBJECT_BASIC_ACCOUNTING_INFORMATION>(), IntPtr.Zero))
{
return false;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

1. Job query failure is silent 📘 Rule violation ◔ Observability

When QueryInformationJobObject fails, TryDisposeIfEmpty silently returns false, leaving the
job handle retained without any diagnostic explaining why cleanup failed. This makes
process-lifetime tracking and native-handle retention failures opaque to users troubleshooting the
new behavior.
Agent Prompt
## Issue description
`TryDisposeIfEmpty` silently returns when `QueryInformationJobObject` fails, so users cannot diagnose why a job handle was not released.

## Issue Context
Capture the Win32 error immediately after the failed native call and log it at the repository-appropriate diagnostic level before returning `false`.

## Fix Focus Areas
- dotnet/src/webdriver/Internal/KillOnCloseJobObject.cs[144-147]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 803a9a7

Driver services no longer attach to the parent's console, so on Windows
the driver (and the browser it spawns) survive when the application is
terminated, e.g. by stopping a debugging session in an IDE.

Restore the lifetime coupling with three mechanisms:
- place the driver process in a job object terminated on close, so the
  operating system kills the driver and browser even when the parent is
  killed abruptly and no cleanup code can run
- terminate leftover drivers when the application exits gracefully
  without disposing them
- add a finalizer terminating the driver process when the service is
  garbage collected without being disposed

Fixes SeleniumHQ#17095
@ashrafiucse

Copy link
Copy Markdown
Author

Fixed in the updated commit: a failed QueryInformationJobObject call is now logged at Trace level with the Win32 error message before returning false, consistent with the other job-object diagnostics. Build, formatting, and the test suite all match the previous state (no new failures vs. the trunk baseline in this environment).

@ashrafiucse
ashrafiucse force-pushed the fix-dotnet-driver-process-leak-on-app-exit branch from 803a9a7 to 21d3641 Compare September 1, 2026 07:42
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 21d3641

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

Labels

C-dotnet .NET Bindings

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[🐛 Bug]: [dotnet] The browser process doesn't close after program is stopped

4 participants