aboutsummaryrefslogtreecommitdiffstats
path: root/src/singleinstance.h
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-29 17:04:06 +0200
committerDanilo M. <danix@danix.xyz>2026-09-29 17:04:06 +0200
commit0cf2c008d46ca2e935987ecfa51a3e712d40420e (patch)
treeac964545fad49513844a36833fc9342a833788d6 /src/singleinstance.h
parente168af8d7d9a500759565719f16ed188ba8a8bcd (diff)
downloadqtmaildir-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.h81
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;
+};