Skip to content

[dotnet] Asynchronously start driver service (breaking change)#17108

Merged
nvborisenko merged 2 commits intoSeleniumHQ:trunkfrom
nvborisenko:dotnet-service-start-async
Feb 18, 2026
Merged

[dotnet] Asynchronously start driver service (breaking change)#17108
nvborisenko merged 2 commits intoSeleniumHQ:trunkfrom
nvborisenko:dotnet-service-start-async

Conversation

@nvborisenko
Copy link
Member

Before:

var service = ChromeDriverService.CreateDefaultService();
service.Start();

After:

var service = ChromeDriverService.CreateDefaultService();
await service.StartAsync();

Intentionally changed method signature (breaking change). I expect only 0.01% of users are affected. Even Appium, who inherits this class, doesn't use this method :)

🔗 Related Issues

Contributes to #17086

💥 What does this PR do?

Refactors the driver service startup process to be fully asynchronous and to support cancellation via CancellationToken. The main changes include updating the driver service start methods to be asynchronous, propagating cancellation tokens throughout the driver discovery and startup process, and updating related method signatures and documentation accordingly.

🔄 Types of changes

  • Cleanup (formatting, renaming)
  • Bug fix (backwards compatible)
  • Breaking change (fix or feature that would cause existing functionality to change)

Copilot AI review requested due to automatic review settings February 18, 2026 16:02
@selenium-ci selenium-ci added the C-dotnet .NET Bindings label Feb 18, 2026
@qodo-code-review
Copy link
Contributor

PR Type

Enhancement


Description

  • Convert driver service startup to async with StartAsync() method

  • Add CancellationToken support throughout driver discovery and startup

  • Update all driver implementations to use async service startup

  • Improve documentation with exception and parameter descriptions


File Walkthrough

Relevant files
Enhancement
DriverService.cs
Convert Start method to async with cancellation support   

dotnet/src/webdriver/DriverService.cs

  • Changed Start() method to async StartAsync(CancellationToken)
    returning ValueTask
  • Added comprehensive XML documentation for parameters and exceptions
  • Replaced synchronous Task.Run().GetAwaiter().GetResult() with direct
    await for driver path discovery
  • Updated WaitForServiceInitializationAsync() call to pass cancellation
    token
+9/-4     
DriverFinder.cs
Add cancellation token support to driver discovery methods

dotnet/src/webdriver/DriverFinder.cs

  • Added CancellationToken parameter to GetDriverPathAsync() and
    GetBrowserPathAsync() methods
  • Updated DiscoverBinaryPathsAsync() to accept and propagate
    cancellation token
  • Added XML documentation for cancellation token parameter
  • Pass cancellation token to SeleniumManager.DiscoverBrowserAsync() call
+8/-6     
ChromiumDriver.cs
Update Chromium driver to use async service startup           

dotnet/src/webdriver/Chromium/ChromiumDriver.cs

  • Changed service.Start() to await
    service.StartAsync().ConfigureAwait(false)
  • Aligns with new async driver service startup pattern
+1/-1     
FirefoxDriver.cs
Update Firefox driver to use async service startup             

dotnet/src/webdriver/Firefox/FirefoxDriver.cs

  • Changed service.Start() to await
    service.StartAsync().ConfigureAwait(false)
  • Aligns with new async driver service startup pattern
+1/-1     
InternetExplorerDriver.cs
Update IE driver to use async service startup                       

dotnet/src/webdriver/IE/InternetExplorerDriver.cs

  • Changed service.Start() to await
    service.StartAsync().ConfigureAwait(false)
  • Aligns with new async driver service startup pattern
+1/-1     
SafariDriver.cs
Update Safari driver to use async service startup               

dotnet/src/webdriver/Safari/SafariDriver.cs

  • Changed service.Start() to await
    service.StartAsync().ConfigureAwait(false)
  • Aligns with new async driver service startup pattern
+1/-1     

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Feb 18, 2026

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Feb 18, 2026

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Introduce a non-breaking async alternative

Instead of replacing DriverService.Start() with StartAsync() and causing a
breaking change, keep the synchronous method. Mark Start() as obsolete and
introduce StartAsync() as a new, non-breaking alternative.

Examples:

dotnet/src/webdriver/DriverService.cs [193-244]
    public async ValueTask StartAsync(CancellationToken cancellationToken = default)
    {
        if (this.driverServiceProcess != null)
        {
            return;
        }

        this.driverServiceProcess = new Process();

        if (this.DriverServicePath != null)

 ... (clipped 42 lines)

Solution Walkthrough:

Before:

// file: dotnet/src/webdriver/DriverService.cs

public abstract class DriverService : IDisposable
{
    // ...
    // The synchronous Start() method is removed.

    [MemberNotNull(nameof(driverServiceProcess))]
    public async ValueTask StartAsync(CancellationToken cancellationToken = default)
    {
        if (this.driverServiceProcess != null)
        {
            return;
        }

        // ... async startup logic ...
        await this.WaitForServiceInitializationAsync(cancellationToken).ConfigureAwait(false);
        // ...
    }
}

After:

// file: dotnet/src/webdriver/DriverService.cs

public abstract class DriverService : IDisposable
{
    // ...

    [Obsolete("Use StartAsync instead. This method will be removed in a future version.")]
    [MemberNotNull(nameof(driverServiceProcess))]
    public void Start()
    {
        StartAsync().GetAwaiter().GetResult();
    }

    [MemberNotNull(nameof(driverServiceProcess))]
    public async ValueTask StartAsync(CancellationToken cancellationToken = default)
    {
        if (this.driverServiceProcess != null)
        {
            return;
        }

        // ... async startup logic ...
        await this.WaitForServiceInitializationAsync(cancellationToken).ConfigureAwait(false);
        // ...
    }
}
Suggestion importance[1-10]: 9

__

Why: This suggestion addresses the core breaking change in the PR's API design, proposing a superior, non-breaking alternative that aligns with best practices for library evolution and ensures backward compatibility.

High
Possible issue
Prevent resource leak on startup failure

To prevent a resource leak, wrap the process creation and startup logic in
StartAsync within a try...catch block, ensuring the Process object is disposed
of in the catch block if an exception occurs.

dotnet/src/webdriver/DriverService.cs [193-244]

 public async ValueTask StartAsync(CancellationToken cancellationToken = default)
 {
     if (this.driverServiceProcess != null)
     {
         return;
     }
 
-    this.driverServiceProcess = new Process();
+    var process = new Process();
+    try
+    {
+        if (this.DriverServicePath != null)
+        {
+            if (this.DriverServiceExecutableName is null)
+            {
+                throw new InvalidOperationException("If the driver service path is specified, the driver service executable name must be as well");
+            }
 
-    if (this.DriverServicePath != null)
-    {
-        if (this.DriverServiceExecutableName is null)
+            process.StartInfo.FileName = Path.Combine(this.DriverServicePath, this.DriverServiceExecutableName);
+        }
+        else
         {
-            throw new InvalidOperationException("If the driver service path is specified, the driver service executable name must be as well");
+            var driverFinder = new DriverFinder(this.GetDefaultDriverOptions());
+            var driverPath = await driverFinder.GetDriverPathAsync(cancellationToken).ConfigureAwait(false);
+            process.StartInfo.FileName = driverPath;
         }
 
-        this.driverServiceProcess.StartInfo.FileName = Path.Combine(this.DriverServicePath, this.DriverServiceExecutableName);
+        process.StartInfo.Arguments = this.CommandLineArguments;
+        process.StartInfo.UseShellExecute = false;
+
+        if (this.EnableProcessRedirection)
+        {
+            process.StartInfo.RedirectStandardOutput = true;
+            process.StartInfo.RedirectStandardError = true;
+            process.OutputDataReceived += this.OnOutputDataReceived;
+            process.ErrorDataReceived += this.OnErrorDataReceived;
+        }
+
+        if (this.HideCommandPromptWindow)
+        {
+            process.StartInfo.CreateNoWindow = true;
+        }
+
+        DriverProcessStartingEventArgs processStartingEventArgs = new DriverProcessStartingEventArgs(process.StartInfo);
+        this.OnDriverProcessStarting(processStartingEventArgs);
+
+        process.Start();
+
+        // Important: Start the process and immediately begin reading the output and error streams to avoid IO deadlocks.
+        process.BeginOutputReadLine();
+        process.BeginErrorReadLine();
+
+        await this.WaitForServiceInitializationAsync(cancellationToken).ConfigureAwait(false);
+
+        this.driverServiceProcess = process;
+
+        DriverProcessStartedEventArgs processStartedEventArgs = new DriverProcessStartedEventArgs(this.driverServiceProcess);
+        this.OnDriverProcessStarted(processStartedEventArgs);
     }
-    else
+    catch
     {
-        var driverFinder = new DriverFinder(this.GetDefaultDriverOptions());
-        var driverPath = await driverFinder.GetDriverPathAsync(cancellationToken).ConfigureAwait(false);
-        this.driverServiceProcess.StartInfo.FileName = driverPath;
+        process.Dispose();
+        throw;
     }
-
-    this.driverServiceProcess.StartInfo.Arguments = this.CommandLineArguments;
-    this.driverServiceProcess.StartInfo.UseShellExecute = false;
-
-    if (this.EnableProcessRedirection)
-    {
-        this.driverServiceProcess.StartInfo.RedirectStandardOutput = true;
-        this.driverServiceProcess.StartInfo.RedirectStandardError = true;
-        this.driverServiceProcess.OutputDataReceived += this.OnOutputDataReceived;
-        this.driverServiceProcess.ErrorDataReceived += this.OnErrorDataReceived;
-    }
-
-    if (this.HideCommandPromptWindow)
-    {
-        this.driverServiceProcess.StartInfo.CreateNoWindow = true;
-    }
-
-    DriverProcessStartingEventArgs processStartingEventArgs = new DriverProcessStartingEventArgs(this.driverServiceProcess.StartInfo);
-    this.OnDriverProcessStarting(processStartingEventArgs);
-
-    this.driverServiceProcess.Start();
-
-    // Important: Start the process and immediately begin reading the output and error streams to avoid IO deadlocks.
-    this.driverServiceProcess.BeginOutputReadLine();
-    this.driverServiceProcess.BeginErrorReadLine();
-
-    await this.WaitForServiceInitializationAsync(cancellationToken).ConfigureAwait(false);
-
-    DriverProcessStartedEventArgs processStartedEventArgs = new DriverProcessStartedEventArgs(this.driverServiceProcess);
-    this.OnDriverProcessStarted(processStartedEventArgs);
 }
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a potential resource leak in the new StartAsync method if an exception occurs during initialization and proposes a robust solution to ensure the Process is disposed.

Medium
General
Propagate cancellation token

Add a CancellationToken parameter to GenerateDriverServiceCommandExecutorAsync
and pass it to the service.StartAsync call to enable cancellation of the driver
startup.

dotnet/src/webdriver/Chromium/ChromiumDriver.cs [147]

-private static async Task<ICommandExecutor> GenerateDriverServiceCommandExecutorAsync(DriverService service, DriverOptions options, TimeSpan commandTimeout)
+private static async Task<ICommandExecutor> GenerateDriverServiceCommandExecutorAsync(DriverService service, DriverOptions options, TimeSpan commandTimeout, CancellationToken cancellationToken = default)

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out that the CancellationToken is not propagated through the call chain, which is a missed opportunity for full asynchronous cancellation support introduced in this PR.

Medium
Add early cancellation check

Add cancellationToken.ThrowIfCancellationRequested() at the beginning of the
StartAsync method to allow for early cancellation before any resources are
allocated.

dotnet/src/webdriver/DriverService.cs [193-200]

 public async ValueTask StartAsync(CancellationToken cancellationToken = default)
 {
+    cancellationToken.ThrowIfCancellationRequested();
+
     if (this.driverServiceProcess != null)
     {
         return;
     }
 
     this.driverServiceProcess = new Process();
  • Apply / Chat
Suggestion importance[1-10]: 4

__

Why: The suggestion provides a valid optimization to fail-fast on cancellation, which improves responsiveness, but it is a minor enhancement rather than a critical fix.

Low
  • Update

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates the .NET WebDriver binding to start driver services asynchronously, enabling cancellation to propagate through driver discovery and the service initialization wait loop.

Changes:

  • Replaces synchronous DriverService.Start() usage with await DriverService.StartAsync(...) in multiple drivers.
  • Refactors DriverService startup to use async driver discovery and async initialization waiting (with CancellationToken support).
  • Adds CancellationToken support to DriverFinder APIs and passes it through to Selenium Manager discovery.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
dotnet/src/webdriver/DriverService.cs Replaces sync start with StartAsync(CancellationToken) and awaits driver discovery + initialization polling.
dotnet/src/webdriver/DriverFinder.cs Adds optional CancellationToken to async path discovery methods and propagates it to Selenium Manager.
dotnet/src/webdriver/Chromium/ChromiumDriver.cs Uses await service.StartAsync() when creating the command executor.
dotnet/src/webdriver/Firefox/FirefoxDriver.cs Uses await service.StartAsync() when creating the command executor.
dotnet/src/webdriver/IE/InternetExplorerDriver.cs Uses await service.StartAsync() when creating the command executor.
dotnet/src/webdriver/Safari/SafariDriver.cs Uses await service.StartAsync() when creating the command executor.
Comments suppressed due to low confidence (1)

dotnet/src/webdriver/DriverService.cs:200

  • StartAsync assigns driverServiceProcess before any awaits. If StartAsync is called concurrently, a second caller can see a non-null driverServiceProcess and return immediately even though the service hasn’t been started/initialized yet (first call is still awaiting). Consider guarding startup with a single in-flight Task/ValueTask (e.g., SemaphoreSlim or a cached start task) so concurrent callers await the same initialization, or only assign driverServiceProcess after the process has started and initialization has completed.
        if (this.driverServiceProcess != null)
        {
            return;
        }

        this.driverServiceProcess = new Process();

@nvborisenko nvborisenko merged commit 88b7673 into SeleniumHQ:trunk Feb 18, 2026
19 checks passed
@nvborisenko nvborisenko deleted the dotnet-service-start-async branch February 18, 2026 17:06
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.

2 participants

Comments