diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-29 17:04:06 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-29 17:04:06 +0200 |
| commit | 0cf2c008d46ca2e935987ecfa51a3e712d40420e (patch) | |
| tree | ac964545fad49513844a36833fc9342a833788d6 /src/singleinstance.h | |
| parent | e168af8d7d9a500759565719f16ed188ba8a8bcd (diff) | |
| download | qtmaildir-0cf2c008d46ca2e935987ecfa51a3e712d40420e.tar.gz qtmaildir-0cf2c008d46ca2e935987ecfa51a3e712d40420e.zip | |
feat: add the single-instance socket
A QLocalServer under the state directory. The first launch listens; a later one
connects, hands over its selectors and exits.
Connect-first ordering, and on Qt 6.11 the probe is the ONLY guard for a live
instance: with UserAccessOption, listen() binds in a private directory and
renames the socket onto the path, which replaces whatever is there, a stale
file and a live socket alike. Measured: with the probe disabled, a second
launch takes the first one's socket and both handover tests fail. The same
rename is what reclaims a stale file after a crash; the removeServer() retry
on AddressInUse is kept for a listen that binds in place.
The server reads each connection asynchronously and parses on disconnect,
rather than blocking in waitForReadyRead() on the UI thread. The client's one
write followed by a disconnect is what marks the payload complete, a reader
past the payload cap is aborted, and a connection that never hangs up is
dropped after two seconds. A connection that writes nothing at all is the other
launch's probe and is ignored without a warning.
A socket that cannot be created does NOT stop the window opening. A read-only
state directory costs single-instance behaviour, which is a degradation; it
must not cost the user their mail client.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'src/singleinstance.h')
| -rw-r--r-- | src/singleinstance.h | 81 |
1 files changed, 81 insertions, 0 deletions
diff --git a/src/singleinstance.h b/src/singleinstance.h new file mode 100644 index 0000000..3f63bfd --- /dev/null +++ b/src/singleinstance.h @@ -0,0 +1,81 @@ +/* + * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs + * Copyright (C) 2026 Danilo M. <danix@danix.xyz> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include <QObject> +#include <QString> + +#include "launchselectors.h" + +class QLocalServer; + +/// Makes a launch either the running instance or a messenger to it (item 200). +/// +/// **This is not network protocol work.** A QLocalServer is a unix domain +/// socket between two copies of this program, owned by the user, in the user's +/// own state directory. The rule in AGENTS.md is about IMAP and SMTP. +/// +/// Knows nothing about queries, accounts or mail: it carries a LaunchSelectors +/// from one process to another and emits what arrived. +class SingleInstance : public QObject +{ + Q_OBJECT + +public: + /// \p socketPath is a filesystem path, so a test can point it inside a + /// QTemporaryDir and never touch the user's real one. + explicit SingleInstance(const QString &socketPath, + QObject *parent = nullptr); + ~SingleInstance() override; + + /// Tries to become the instance others talk to. + /// + /// Returns true when this process is now listening, false when another + /// instance already is OR when no socket could be created at all. The + /// caller treats both falses the same way for the second case: **a socket + /// that cannot be created must not stop the window opening**, or a + /// read-only state directory costs the user their mail client. + /// + /// Handles the stale socket file, which is the ordinary aftermath of a + /// crash: it attempts a CONNECTION first, and a refused connection on an + /// existing file proves nothing is serving it, so the file may be + /// replaced. Connecting first is what stops a live instance being removed + /// out from under itself, and it is the ONLY guard: listen() replaces a + /// taken path rather than refusing it (see the .cpp). + bool tryBecomeServer(); + + /// True when tryBecomeServer() succeeded and this process is listening. + bool isServer() const; + + /// Sends \p selectors to the running instance. Returns false when there is + /// none, or when the write could not be completed. + /// + /// An EMPTY selector set is still sent: a bare `qtmaildir` against a + /// running window means "raise yourself", which is a request and not a + /// no-op. + bool sendToRunningInstance(const LaunchSelectors &selectors); + +signals: + /// A later launch handed these over. Emitted on the server side only. + void selectorsReceived(const LaunchSelectors &selectors); + +private: + QString m_socketPath; + QLocalServer *m_server = nullptr; +}; |
