namespace CursorLang.Core.Services; /// /// Tells the settings window that the agent is quitting and it is to close with it. /// /// /// The one thing the agent says to the settings window, and the mirror image of /// . "Exit" in the tray menu means the application is done /// with, and a settings window left alone on the screen after it is a window belonging /// to nothing: the tray icon it was opened from is gone, and closing it would be the /// user's only remaining move. /// /// A named event rather than a window message, because the agent has no handle to send /// one to: the settings window lives in a process the agent starts and deliberately /// does not keep hold of. The name has no Global prefix, so it lives in the /// session namespace — same reasoning as , and the same /// consequence: with fast user switching each user's halves talk to their own. /// /// Only the settings window creates the object; the agent opens what is already there /// and stays silent when there is nothing. Were it the other way round, the request /// would sit in an auto-reset event waiting for the next settings window to open and /// close it the moment it did. /// internal sealed class SettingsCloseSignal : IDisposable { private const string EventName = "CursorLang.CloseSettings"; private readonly string _eventName; private EventWaitHandle? _request; private RegisteredWaitHandle? _wait; /// /// Listens on the name the two halves agree on. /// /// /// A namespace of its own. Empty for the application; the tests pass one so that /// they do not answer for — or worse, close — a settings window someone is using. /// internal SettingsCloseSignal(string nameSuffix = "") => _eventName = EventName + nameSuffix; /// The agent asks for the window to be closed. /// /// Raised on a thread pool thread, wherever the wait happened to be answered — a /// window obeys only its own, so the handler has to get back to it. /// internal event EventHandler? CloseRequested; /// /// Asks the settings window of this session, if one is open, to close. Silence is /// a normal answer: most of the time the user quits with no window on the screen. /// /// Whether there was anybody to hear it. internal static bool RequestClose(string nameSuffix = "") { if (!EventWaitHandle.TryOpenExisting(EventName + nameSuffix, out EventWaitHandle? request)) { return false; } using (request) { return request.Set(); } } /// Starts waiting for the request. Called once, by the settings window. internal void Listen() { if (_request is not null) { return; } _request = new EventWaitHandle(false, EventResetMode.AutoReset, _eventName); // As in the gate: the thread pool holds the wait, there is no reason to keep a // thread of our own for a request that may never come _wait = ThreadPool.RegisterWaitForSingleObject( _request, OnCloseSignalled, state: null, Timeout.Infinite, executeOnlyOnce: false); } public void Dispose() { _wait?.Unregister(null); _wait = null; _request?.Dispose(); _request = null; } private void OnCloseSignalled(object? state, bool timedOut) => CloseRequested?.Invoke(this, EventArgs.Empty); }