Skip to content

Implement graceful shutdown for AppiumLocalService on Windows to prevent temp file accumulation#172

Open
Copilot wants to merge 4 commits intomainfrom
copilot/implement-graceful-shutdown
Open

Implement graceful shutdown for AppiumLocalService on Windows to prevent temp file accumulation#172
Copilot wants to merge 4 commits intomainfrom
copilot/implement-graceful-shutdown

Conversation

Copy link

Copilot AI commented Dec 14, 2025

List of changes

AppiumLocalService now sends CTRL+C to Node.js processes on Windows before forceful termination, allowing cleanup handlers to execute and preventing accumulation of temporary .response files.

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change that adds functionality or value)
  • Breaking change (fix or feature that would cause existing functionality not to work as expected)
  • Test fix (non-breaking change that improves test stability or correctness)

Documentation

  • Have you proposed a file change/ PR with Appium to update documentation?

This can be done by navigating to the documentation section on http://appium.io selecting the appropriate command/endpoint and clicking the 'Edit this doc' link to update documentation

Integration tests

  • Have you provided integration tests for your changes? (required for Bugfix, New feature, or Test fix)

Existing integration tests for service start/stop cover this change. The graceful shutdown path is exercised by all tests that call service.Dispose().

Details

Problem

Process.Kill() on Windows acts as SIGKILL, preventing Node.js exit handlers from running. Result: temporary .response files accumulate indefinitely (300GB+ reported).

Solution

Added TryGracefulShutdownOnWindows() that:

  • Uses P/Invoke to attach to process console
  • Sends CTRL+C event (SIGINT equivalent)
  • Waits 5 seconds for graceful exit
  • Falls back to Kill() on failure or non-Windows platforms

Implementation

private void DestroyProcess()
{
    if (Service == null) return;
    
    try
    {
        // Graceful shutdown on Windows, fallback to Kill() if unsupported/fails
        if (!TryGracefulShutdownOnWindows(Service))
        {
            Service.Kill();
        }
    }
    catch { }
    finally
    {
        Service?.Close();
        SharedHttpClient.Dispose();
    }
}

Platform behavior:

  • Windows: CTRL+C → wait 5s → Kill() if needed
  • Linux/macOS: Existing behavior unchanged (already handles SIGTERM correctly)

P/Invoke APIs used: AttachConsole, GenerateConsoleCtrlEvent, FreeConsole, SetConsoleCtrlHandler

Original prompt

This section details on the original issue you should resolve

<issue_title>[Feat]: Implement graceful shutdown (SIGINT/SIGTERM) for AppiumLocalService on Windows to prevent temp file accumulation</issue_title>
<issue_description>Problem Description
The AppiumLocalService in the .NET client currently uses Process.Kill() (or Dispose()) to stop the local Appium server instance. On Windows, this behaves as a SIGKILL (force termination).

Because the process is force-killed, the Appium Node.js server does not trigger its cleanup logic. As a result, temporary .response files (used for idempotency keys) generated during the session are not deleted. Over time, these files accumulate and can consume significant disk space (reports of >300GB).

Technical Context
Source Issue: appium/dotnet-client#625

Affected Platform: Windows (macOS/Linux handle SIGTERM correctly via Dispose).

Root Cause: The Kill() method does not allow the Node.js process to run its exit or SIGINT handlers.

Failed Attempts:

Process.CloseMainWindow(): Does not work if the server is started without a visible window (background process).

GenerateConsoleCtrlEvent: Unreliable for background processes started via AppiumLocalService.

Requirements
We need to implement a mechanism in AppiumLocalService that sends a graceful termination signal (equivalent to SIGINT or SIGTERM) to the Node.js process on Windows.

Potential Solutions to Explore
Window Management: Start the process with a visible window but immediately hide it via P/Invoke, allowing CloseMainWindow to function correctly later.

IPC/Signal: Use a dedicated mechanism or P/Invoke to send CTRL_C_EVENT specifically to the child Node process group.

Process Management: Review how other .NET wrappers for Node.js services handle graceful shutdowns on Windows.

Reproduction
Start an AppiumLocalService instance on Windows.

Run a test that generates session requests (creating .response temp files).

Call service.Dispose().

Observe that the Appium process dies immediately and .response files remain in the temp folder.</issue_description>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 3 commits December 14, 2025 22:11
Co-authored-by: Dor-bl <59066376+Dor-bl@users.noreply.github.com>
Co-authored-by: Dor-bl <59066376+Dor-bl@users.noreply.github.com>
Co-authored-by: Dor-bl <59066376+Dor-bl@users.noreply.github.com>
Copilot AI changed the title [WIP] Add graceful shutdown for AppiumLocalService on Windows Implement graceful shutdown for AppiumLocalService on Windows to prevent temp file accumulation Dec 14, 2025
Copilot AI requested a review from Dor-bl December 14, 2025 22:42
@Dor-bl Dor-bl marked this pull request as ready for review February 7, 2026 22:02
Copilot AI review requested due to automatic review settings February 7, 2026 22:02
Copy link

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

Adds a Windows-specific graceful shutdown path for AppiumLocalService so Node/Appium can run its exit handlers (preventing accumulation of temporary .response files) before falling back to a forceful terminate.

Changes:

  • Introduces a TryGracefulShutdownOnWindows helper that attempts to send a CTRL+C event to the Appium/Node process.
  • Updates DestroyProcess() to prefer graceful shutdown on Windows, falling back to Process.Kill() if needed.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +188 to +196
// Disable Ctrl-C handling for our own process to prevent it from being terminated
// when we send CTRL+C to the target process
SetConsoleCtrlHandler(null, true);

// Attach to the target process console
if (!AttachConsole((uint)process.Id))
{
return false;
}
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

SetConsoleCtrlHandler(null, true) is not reliably reverted: if AttachConsole fails (or GenerateConsoleCtrlEvent returns false before the re-enable call), the method returns early with Ctrl-C handling still disabled for the current process. This can leave the host test runner ignoring Ctrl+C for the rest of its lifetime. Use a try/finally (with a flag) to always restore the handler state before any return, and ensure any console attachment is cleaned up as well.

Copilot uses AI. Check for mistakes.
Comment on lines +192 to +196
// Attach to the target process console
if (!AttachConsole((uint)process.Id))
{
return false;
}
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

AttachConsole will fail with ERROR_ACCESS_DENIED if the current process is already attached to a console (common for dotnet test / CLI runs). Currently there is no FreeConsole() call before AttachConsole, so the graceful shutdown path will often never execute and will always fall back to Kill(). Consider calling FreeConsole() before AttachConsole (ignore failure if not attached), and restore the original console state in a finally.

Copilot uses AI. Check for mistakes.
Comment on lines +217 to +229
catch (Win32Exception)
{
// Windows API call failed, return false to fallback to Kill()
try
{
FreeConsole();
SetConsoleCtrlHandler(null, false);
}
catch (Win32Exception)
{
// Ignore cleanup errors - we're already in error handling
}
}
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

The Win32 APIs invoked via P/Invoke (AttachConsole, GenerateConsoleCtrlEvent, FreeConsole, SetConsoleCtrlHandler) return bool and will not throw Win32Exception by themselves. The catch (Win32Exception) blocks therefore won’t run, and the cleanup logic inside them is effectively dead. Prefer putting cleanup/restoration in a finally and (if needed) use Marshal.GetLastWin32Error() for diagnostics when a call returns false.

Copilot uses AI. Check for mistakes.
Comment on lines +198 to +207
// Send CTRL+C event
if (!GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0))
{
FreeConsole();
return false;
}

// Detach from the process console
FreeConsole();

Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

GenerateConsoleCtrlEvent is asynchronous. Detaching from the console immediately after generating the event can make delivery unreliable. Consider keeping the console attached briefly (e.g., a short sleep) before FreeConsole(), or otherwise ensure the event has time to be delivered before detaching.

Copilot uses AI. Check for mistakes.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

using System.Threading; appears unused in this file as-is (no Thread/WaitHandle usages). If you don't end up needing it, remove it to avoid unnecessary usings/warnings.

Suggested change
using System.Threading.Tasks;

Copilot uses AI. Check for mistakes.
FreeConsole();

// Re-enable Ctrl-C handling for our own process
SetConsoleCtrlHandler(null, false);
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

Replace this call with a call to managed code if possible.

Copilot uses AI. Check for mistakes.
private const int CTRL_C_EVENT = 0;

[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool AttachConsole(uint dwProcessId);
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

Minimise the use of unmanaged code.

Copilot uses AI. Check for mistakes.
private static extern bool AttachConsole(uint dwProcessId);

[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool FreeConsole();
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

Minimise the use of unmanaged code.

Copilot uses AI. Check for mistakes.
private static extern bool FreeConsole();

[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetConsoleCtrlHandler(ConsoleCtrlDelegate HandlerRoutine, bool Add);
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

Minimise the use of unmanaged code.

Copilot uses AI. Check for mistakes.
private static extern bool SetConsoleCtrlHandler(ConsoleCtrlDelegate HandlerRoutine, bool Add);

[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GenerateConsoleCtrlEvent(uint dwCtrlEvent, uint dwProcessGroupId);
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

Minimise the use of unmanaged code.

Copilot uses AI. Check for mistakes.
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.

[Feat]: Implement graceful shutdown (SIGINT/SIGTERM) for AppiumLocalService on Windows to prevent temp file accumulation

2 participants