Single instance
This commit is contained in:
37
src/main.cpp
37
src/main.cpp
@@ -15,6 +15,7 @@
|
||||
#include "keyrow.h"
|
||||
#include "mapping.h"
|
||||
#include "mappings.h"
|
||||
#include "singleinstance.h"
|
||||
|
||||
#ifndef PACKAGE
|
||||
#define PACKAGE "gkeymapper"
|
||||
@@ -129,7 +130,9 @@ void on_capture_clicked(GtkButton* btn, gpointer userData) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sel == 0) {
|
||||
// "Key" and "Key (hold)" capture the same thing: a hold mapping is a single
|
||||
// step that the daemon keeps down while the G key is held.
|
||||
if (sel == 0 || sel == 3) {
|
||||
gkeymapper::open_key_capture_dialog(row, window);
|
||||
}
|
||||
}
|
||||
@@ -144,6 +147,10 @@ static void update_row_state(KeyRow* row) {
|
||||
gtk_button_set_label(GTK_BUTTON(row->captureButton), _("Choose file..."));
|
||||
gtk_entry_set_placeholder_text(GTK_ENTRY(row->targetEntry),
|
||||
_("e.g. /usr/bin/firefox or custom command"));
|
||||
} else if (sel == 3) {
|
||||
gtk_button_set_label(GTK_BUTTON(row->captureButton), _("Capture"));
|
||||
gtk_entry_set_placeholder_text(GTK_ENTRY(row->targetEntry),
|
||||
_("e.g. ctrl+w (held while the G key is held)"));
|
||||
} else {
|
||||
gtk_button_set_label(GTK_BUTTON(row->captureButton), _("Capture"));
|
||||
gtk_entry_set_placeholder_text(GTK_ENTRY(row->targetEntry),
|
||||
@@ -302,6 +309,12 @@ static void action_about(GSimpleAction* /*action*/, GVariant* /*parameter*/, gpo
|
||||
}
|
||||
}
|
||||
|
||||
// Exported by GApplication as org.gtk.Actions, so a new start can close this
|
||||
// instance over D-Bus before it takes over (see singleinstance.h).
|
||||
static void action_quit(GSimpleAction* /*action*/, GVariant* /*parameter*/, gpointer userData) {
|
||||
g_application_quit(G_APPLICATION(userData));
|
||||
}
|
||||
|
||||
static std::string find_asset(const std::string& name) {
|
||||
const char* env = g_getenv("GKEYMAPPER_ASSETS_DIR");
|
||||
if (env && *env) {
|
||||
@@ -395,7 +408,8 @@ void build_ui(GtkApplication* app) {
|
||||
gtk_grid_attach(GTK_GRID(grid), h, c, 0, 1, 1);
|
||||
}
|
||||
|
||||
const char* typeOptions[] = { _("Key"), _("Command"), _("Disabled"), nullptr };
|
||||
const char* typeOptions[] = { _("Key"), _("Command"), _("Disabled"),
|
||||
_("Key (hold)"), nullptr };
|
||||
|
||||
rows.resize(kNumKeys);
|
||||
for (int i = 0; i < kNumKeys; ++i) {
|
||||
@@ -484,6 +498,15 @@ void build_ui(GtkApplication* app) {
|
||||
}
|
||||
|
||||
static void on_activate(GtkApplication* app, gpointer /*user_data*/) {
|
||||
// One window, always. A second "activate" (a desktop launch, or D-Bus
|
||||
// activation while the app is open) must not build another window - main()
|
||||
// replaces a running instance before this runs, so normally there is none.
|
||||
GList* windows = gtk_application_get_windows(app);
|
||||
if (windows != nullptr) {
|
||||
gtk_window_present(GTK_WINDOW(windows->data));
|
||||
return;
|
||||
}
|
||||
|
||||
GSimpleAction* exportAction = g_simple_action_new("export-config", NULL);
|
||||
g_signal_connect(exportAction, "activate", G_CALLBACK(action_export), app);
|
||||
g_action_map_add_action(G_ACTION_MAP(app), G_ACTION(exportAction));
|
||||
@@ -496,6 +519,10 @@ static void on_activate(GtkApplication* app, gpointer /*user_data*/) {
|
||||
g_signal_connect(aboutAction, "activate", G_CALLBACK(action_about), app);
|
||||
g_action_map_add_action(G_ACTION_MAP(app), G_ACTION(aboutAction));
|
||||
|
||||
GSimpleAction* quitAction = g_simple_action_new("quit", NULL);
|
||||
g_signal_connect(quitAction, "activate", G_CALLBACK(action_quit), app);
|
||||
g_action_map_add_action(G_ACTION_MAP(app), G_ACTION(quitAction));
|
||||
|
||||
build_ui(app);
|
||||
}
|
||||
} // namespace
|
||||
@@ -511,6 +538,12 @@ int main(int argc, char** argv) {
|
||||
bind_textdomain_codeset(PACKAGE, "UTF-8");
|
||||
textdomain(PACKAGE);
|
||||
|
||||
// One GUI only, and it is the one that was just started: close the instance
|
||||
// that already owns the app id (an older build, or the same one) before
|
||||
// becoming the primary instance. Without this the new process just hands over
|
||||
// to the old one and its stale window stays on screen.
|
||||
gkeymapper::replace_running_instance(gkeymapper::ipc::kAppId);
|
||||
|
||||
GtkApplication* app = gtk_application_new(gkeymapper::ipc::kAppId,
|
||||
G_APPLICATION_FLAGS_NONE);
|
||||
g_signal_connect(app, "activate", G_CALLBACK(on_activate), nullptr);
|
||||
|
||||
@@ -20,6 +20,7 @@ bool write_mappings(const std::string& path, const std::vector<KeyRow>& rows) {
|
||||
switch (gtk_drop_down_get_selected(GTK_DROP_DOWN(row.typeDrop))) {
|
||||
case 0: mapping.type = ActionType::Key; break;
|
||||
case 1: mapping.type = ActionType::Command; break;
|
||||
case 3: mapping.type = ActionType::KeyHold; break;
|
||||
default: mapping.type = ActionType::Disabled; break;
|
||||
}
|
||||
const char* target = gtk_editable_get_text(GTK_EDITABLE(row.targetEntry));
|
||||
@@ -45,6 +46,7 @@ void read_mappings(const std::string& path, const std::vector<KeyRow>& rows) {
|
||||
switch (set.keys[row.index].type) {
|
||||
case ActionType::Key: idx = 0; break;
|
||||
case ActionType::Command: idx = 1; break;
|
||||
case ActionType::KeyHold: idx = 3; break;
|
||||
default: idx = 2; break;
|
||||
}
|
||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(row.typeDrop), idx);
|
||||
|
||||
157
src/singleinstance.cpp
Normal file
157
src/singleinstance.cpp
Normal file
@@ -0,0 +1,157 @@
|
||||
// gKeyMapper - keep exactly one GUI: a new start replaces the running instance.
|
||||
//
|
||||
// GtkApplication already makes a second process hand over to the first one (the
|
||||
// app id is unique), but that leaves the *old* window on screen - including one
|
||||
// from an earlier build. So main() first closes what is running and only then
|
||||
// becomes the primary instance itself.
|
||||
|
||||
#include "singleinstance.h"
|
||||
|
||||
#include <csignal>
|
||||
|
||||
#include <gio/gio.h>
|
||||
#include <glib.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace gkeymapper {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kCallTimeoutMs = 1000;
|
||||
constexpr int kQuitWaitMs = 1500;
|
||||
constexpr int kTermWaitMs = 1500;
|
||||
constexpr int kPollMs = 50;
|
||||
|
||||
GDBusConnection* session_bus() {
|
||||
GError* error = nullptr;
|
||||
GDBusConnection* bus = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error);
|
||||
if (bus == nullptr) {
|
||||
g_printerr("gkeymapper: no session bus (%s)\n", error != nullptr ? error->message : "?");
|
||||
g_clear_error(&error);
|
||||
}
|
||||
return bus;
|
||||
}
|
||||
|
||||
// Sync call with a short timeout; nullptr means "no answer" (the caller decides
|
||||
// what that means) and the error is dropped - a missing action or an absent name
|
||||
// is an expected outcome here, not a failure to report.
|
||||
GVariant* call(GDBusConnection* bus, const char* name, const char* path, const char* interface,
|
||||
const char* method, GVariant* args, const GVariantType* replyType) {
|
||||
GError* error = nullptr;
|
||||
GVariant* reply = g_dbus_connection_call_sync(bus, name, path, interface, method, args,
|
||||
replyType, G_DBUS_CALL_FLAGS_NONE,
|
||||
kCallTimeoutMs, nullptr, &error);
|
||||
g_clear_error(&error);
|
||||
return reply;
|
||||
}
|
||||
|
||||
bool has_owner(GDBusConnection* bus, const std::string& name) {
|
||||
GVariant* reply = call(bus, "org.freedesktop.DBus", "/org/freedesktop/DBus",
|
||||
"org.freedesktop.DBus", "NameHasOwner",
|
||||
g_variant_new("(s)", name.c_str()), G_VARIANT_TYPE("(b)"));
|
||||
if (reply == nullptr) {
|
||||
return false;
|
||||
}
|
||||
gboolean owned = FALSE;
|
||||
g_variant_get(reply, "(b)", &owned);
|
||||
g_variant_unref(reply);
|
||||
return owned;
|
||||
}
|
||||
|
||||
bool wait_until_free(GDBusConnection* bus, const std::string& name, int timeoutMs) {
|
||||
for (int waited = 0; waited <= timeoutMs; waited += kPollMs) {
|
||||
if (!has_owner(bus, name)) {
|
||||
return true;
|
||||
}
|
||||
g_usleep(kPollMs * 1000);
|
||||
}
|
||||
return !has_owner(bus, name);
|
||||
}
|
||||
|
||||
// GApplication exports the app actions under the object path derived from the app
|
||||
// id (dots become slashes), so "quit" is reachable before GTK is even started.
|
||||
std::string action_object_path(const std::string& appId) {
|
||||
std::string path = "/";
|
||||
for (char c : appId) {
|
||||
path += c == '.' ? '/' : c;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
bool ask_to_quit(GDBusConnection* bus, const std::string& name) {
|
||||
GVariantBuilder parameters;
|
||||
g_variant_builder_init(¶meters, G_VARIANT_TYPE("av"));
|
||||
GVariantBuilder platform;
|
||||
g_variant_builder_init(&platform, G_VARIANT_TYPE("a{sv}"));
|
||||
const std::string path = action_object_path(name);
|
||||
GVariant* reply =
|
||||
call(bus, name.c_str(), path.c_str(), "org.gtk.Actions", "Activate",
|
||||
g_variant_new("(sava{sv})", "quit", ¶meters, &platform), nullptr);
|
||||
if (reply == nullptr) {
|
||||
return false; // no such action (older build) or no answer
|
||||
}
|
||||
g_variant_unref(reply);
|
||||
return true;
|
||||
}
|
||||
|
||||
pid_t owner_pid(GDBusConnection* bus, const std::string& name) {
|
||||
GVariant* reply = call(bus, "org.freedesktop.DBus", "/org/freedesktop/DBus",
|
||||
"org.freedesktop.DBus", "GetConnectionUnixProcessID",
|
||||
g_variant_new("(s)", name.c_str()), G_VARIANT_TYPE("(u)"));
|
||||
if (reply == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
guint32 pid = 0;
|
||||
g_variant_get(reply, "(u)", &pid);
|
||||
g_variant_unref(reply);
|
||||
return static_cast<pid_t>(pid);
|
||||
}
|
||||
|
||||
// The bus name only yields a pid; check that it is our GUI before signalling it,
|
||||
// so a wedged or recycled pid is never hit.
|
||||
bool is_our_gui(pid_t pid) {
|
||||
gchar* path = g_strdup_printf("/proc/%d/comm", static_cast<int>(pid));
|
||||
gchar* contents = nullptr;
|
||||
const bool read = g_file_get_contents(path, &contents, nullptr, nullptr);
|
||||
g_free(path);
|
||||
std::string comm = read && contents != nullptr ? contents : "";
|
||||
g_free(contents);
|
||||
while (!comm.empty() && (comm.back() == '\n' || comm.back() == '\r')) {
|
||||
comm.pop_back();
|
||||
}
|
||||
return comm == "gkeymapper";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool replace_running_instance(const std::string& appId) {
|
||||
GDBusConnection* bus = session_bus();
|
||||
if (bus == nullptr) {
|
||||
return true; // without a bus there is no other instance either
|
||||
}
|
||||
|
||||
bool free = !has_owner(bus, appId);
|
||||
if (!free) {
|
||||
// Clean path: the running instance quits itself.
|
||||
ask_to_quit(bus, appId);
|
||||
free = wait_until_free(bus, appId, kQuitWaitMs);
|
||||
if (!free) {
|
||||
// An instance from before the "quit" action existed, or a hung one.
|
||||
const pid_t pid = owner_pid(bus, appId);
|
||||
if (pid > 0 && is_our_gui(pid)) {
|
||||
g_printerr("gkeymapper: closing the running instance (pid %d)\n",
|
||||
static_cast<int>(pid));
|
||||
::kill(pid, SIGTERM);
|
||||
}
|
||||
free = wait_until_free(bus, appId, kTermWaitMs);
|
||||
}
|
||||
}
|
||||
|
||||
g_object_unref(bus);
|
||||
if (!free) {
|
||||
g_printerr("gkeymapper: another instance still holds %s\n", appId.c_str());
|
||||
}
|
||||
return free;
|
||||
}
|
||||
|
||||
} // namespace gkeymapper
|
||||
23
src/singleinstance.h
Normal file
23
src/singleinstance.h
Normal file
@@ -0,0 +1,23 @@
|
||||
// gKeyMapper - keep exactly one GUI: a new start replaces the running instance.
|
||||
#ifndef GKEYMAPPER_SINGLEINSTANCE_H
|
||||
#define GKEYMAPPER_SINGLEINSTANCE_H
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace gkeymapper {
|
||||
|
||||
// Asks the instance that owns `appId` on the session bus to quit and waits until
|
||||
// it has let go of the name, so the caller can become the only instance. The
|
||||
// request goes through the app's `quit` action (exported by GApplication as
|
||||
// org.gtk.Actions); an instance that does not offer it — an older build — is
|
||||
// terminated with SIGTERM instead, after checking that the bus name really
|
||||
// belongs to a gkeymapper process.
|
||||
//
|
||||
// Returns true when the name is free afterwards, which is also the case when
|
||||
// nobody owned it. The caller still runs its usual single-instance logic, so a
|
||||
// `false` (a hung instance that cannot be closed) only means "no new window".
|
||||
bool replace_running_instance(const std::string& appId);
|
||||
|
||||
} // namespace gkeymapper
|
||||
|
||||
#endif // GKEYMAPPER_SINGLEINSTANCE_H
|
||||
Reference in New Issue
Block a user