Implement graceful shutdown for AppiumLocalService on Windows to prevent temp file accumulation#172
Implement graceful shutdown for AppiumLocalService on Windows to prevent temp file accumulation#172
Conversation
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>
There was a problem hiding this comment.
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
TryGracefulShutdownOnWindowshelper that attempts to send a CTRL+C event to the Appium/Node process. - Updates
DestroyProcess()to prefer graceful shutdown on Windows, falling back toProcess.Kill()if needed.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // 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; | ||
| } |
There was a problem hiding this comment.
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.
| // Attach to the target process console | ||
| if (!AttachConsole((uint)process.Id)) | ||
| { | ||
| return false; | ||
| } |
There was a problem hiding this comment.
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.
| 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| // Send CTRL+C event | ||
| if (!GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0)) | ||
| { | ||
| FreeConsole(); | ||
| return false; | ||
| } | ||
|
|
||
| // Detach from the process console | ||
| FreeConsole(); | ||
|
|
There was a problem hiding this comment.
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.
| using System.Runtime.CompilerServices; | ||
| using System.Runtime.InteropServices; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; |
There was a problem hiding this comment.
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.
| using System.Threading.Tasks; |
| FreeConsole(); | ||
|
|
||
| // Re-enable Ctrl-C handling for our own process | ||
| SetConsoleCtrlHandler(null, false); |
There was a problem hiding this comment.
Replace this call with a call to managed code if possible.
| private const int CTRL_C_EVENT = 0; | ||
|
|
||
| [DllImport("kernel32.dll", SetLastError = true)] | ||
| private static extern bool AttachConsole(uint dwProcessId); |
There was a problem hiding this comment.
Minimise the use of unmanaged code.
| private static extern bool AttachConsole(uint dwProcessId); | ||
|
|
||
| [DllImport("kernel32.dll", SetLastError = true)] | ||
| private static extern bool FreeConsole(); |
There was a problem hiding this comment.
Minimise the use of unmanaged code.
| private static extern bool FreeConsole(); | ||
|
|
||
| [DllImport("kernel32.dll", SetLastError = true)] | ||
| private static extern bool SetConsoleCtrlHandler(ConsoleCtrlDelegate HandlerRoutine, bool Add); |
There was a problem hiding this comment.
Minimise the use of unmanaged code.
| private static extern bool SetConsoleCtrlHandler(ConsoleCtrlDelegate HandlerRoutine, bool Add); | ||
|
|
||
| [DllImport("kernel32.dll", SetLastError = true)] | ||
| private static extern bool GenerateConsoleCtrlEvent(uint dwCtrlEvent, uint dwProcessGroupId); |
There was a problem hiding this comment.
Minimise the use of unmanaged code.
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
.responsefiles.Types of changes
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
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.responsefiles accumulate indefinitely (300GB+ reported).Solution
Added
TryGracefulShutdownOnWindows()that:Kill()on failure or non-Windows platformsImplementation
Platform behavior:
P/Invoke APIs used:
AttachConsole,GenerateConsoleCtrlEvent,FreeConsole,SetConsoleCtrlHandlerOriginal prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.