From e58e609be51506978969743852a32af209a1babd Mon Sep 17 00:00:00 2001 From: Matthijs Kooijman Date: Mon, 4 Dec 2023 17:02:11 +0100 Subject: [PATCH 01/11] Fix quit dbus action for the GUI This action was registered, but did not work because the handler accepted too few arguments and also called itself recursively. This fixes both, calling the 'quit' action (as exposed through dbus by gtk automatically) now actually quits the GUI. --- src/hamster-cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hamster-cli.py b/src/hamster-cli.py index 9c6786787..9351bbfdf 100755 --- a/src/hamster-cli.py +++ b/src/hamster-cli.py @@ -148,8 +148,8 @@ def on_activate(self, data=None): def on_activate_window(self, action=None, data=None): self._open_window(action.get_name(), data) - def on_activate_quit(self, data=None): - self.on_activate_quit() + def on_activate_quit(self, action=None, data=None): + self.quit() def on_startup(self, data=None): logger.debug("startup") From 76bed1d66e7c60cdf2bba6594877f97f6849d32c Mon Sep 17 00:00:00 2001 From: Matthijs Kooijman Date: Mon, 4 Dec 2023 17:11:49 +0100 Subject: [PATCH 02/11] hamster-cli: Flatten commandline handling flow This simplifies the flow a bit by putting the handling of "add" with arguments at the same level of all other application actions instead of below it, needing one fewer level of indentation. Behavior should be fully unchanged. Diff best viewed while ignoring whitespace changes. --- src/hamster-cli.py | 45 ++++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/src/hamster-cli.py b/src/hamster-cli.py index 9351bbfdf..58c51c91c 100755 --- a/src/hamster-cli.py +++ b/src/hamster-cli.py @@ -488,30 +488,29 @@ def version(self): else: action = args.action - if action in ("about", "add", "edit", "overview", "preferences"): - if action == "add" and args.action_args: - assert not unknown_args, "unknown options: {}".format(unknown_args) - # directly add fact from arguments - id_ = hamster_client.start(*args.action_args) - assert id_ > 0, "failed to add fact" - sys.exit(0) + if action == "add" and args.action_args: + assert not unknown_args, "unknown options: {}".format(unknown_args) + # directly add fact from arguments + id_ = hamster_client.start(*args.action_args) + assert id_ > 0, "failed to add fact" + sys.exit(0) + elif action in ("about", "add", "edit", "overview", "preferences"): + app.register() + if action == "edit": + assert len(args.action_args) == 1, ( + "edit requires exactly one argument, got {}" + .format(args.action_args)) + id_ = int(args.action_args[0]) + assert id_ > 0, "received non-positive id : {}".format(id_) + action_data = glib.Variant.new_int32(id_) else: - app.register() - if action == "edit": - assert len(args.action_args) == 1, ( - "edit requires exactly one argument, got {}" - .format(args.action_args)) - id_ = int(args.action_args[0]) - assert id_ > 0, "received non-positive id : {}".format(id_) - action_data = glib.Variant.new_int32(id_) - else: - action_data = None - app.activate_action(action, action_data) - run_args = [sys.argv[0]] + unknown_args - logger.debug("run {}".format(run_args)) - status = app.run(run_args) - logger.debug("app exited") - sys.exit(status) + action_data = None + app.activate_action(action, action_data) + run_args = [sys.argv[0]] + unknown_args + logger.debug("run {}".format(run_args)) + status = app.run(run_args) + logger.debug("app exited") + sys.exit(status) elif hasattr(hamster_client, action): getattr(hamster_client, action)(*args.action_args) else: From 4e78ca6df641d3b57e459afcc1f5847bd81521b1 Mon Sep 17 00:00:00 2001 From: Matthijs Kooijman Date: Mon, 25 Dec 2023 11:46:12 +0100 Subject: [PATCH 03/11] Print "hamster-service up" later and use logger This logs the message after the storage class was actually instantiated and registered on the bus, rather than before. --- src/hamster-service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hamster-service.py b/src/hamster-service.py index 29e7c1e06..1500083cd 100755 --- a/src/hamster-service.py +++ b/src/hamster-service.py @@ -460,6 +460,6 @@ def Version(self): # hamster_logger for the rest hamster_logger.setLevel(args.log_level) - print("hamster-service up") storage = Storage(loop) + logger.info("hamster-service up") loop.run() From 3a5aacf1b99fe36663636b72a489eaa6192808fe Mon Sep 17 00:00:00 2001 From: Matthijs Kooijman Date: Mon, 25 Dec 2023 11:53:12 +0100 Subject: [PATCH 04/11] hamster-windows-service: Use logger This converts the single print in this script to use a logger instance (like hamster-service already uses). --- src/hamster-windows-service.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/hamster-windows-service.py b/src/hamster-windows-service.py index 37bcad907..c0ad5bf7b 100755 --- a/src/hamster-windows-service.py +++ b/src/hamster-windows-service.py @@ -10,7 +10,9 @@ from gi.repository import GLib as glib import hamster +from hamster.lib import default_logger +logger = default_logger(__file__) DBusGMainLoop(set_as_default=True) loop = glib.MainLoop() @@ -84,7 +86,6 @@ def preferences(self): glib.set_prgname(str(_("hamster-windows-service"))) window_server = WindowServer(loop) - - print("hamster-window-service up") + logger.info("hamster-window-service up") loop.run() From 0fbd1ad44a685e968c91cf10e3ec290d74aa06dc Mon Sep 17 00:00:00 2001 From: Matthijs Kooijman Date: Mon, 25 Dec 2023 12:01:37 +0100 Subject: [PATCH 05/11] Fix dbus name claiming race condition This refactors the way that dbus names are claimed for hamster-service and hamster-window-service. Instead of checking if the name is already claimed beforehand, this tries to claim the and checks the result. This fixes a minor race condition when a service is started twice at the same time. Before, the name would appear to be free when it was checked, but when the script proceeded to actually claim the name, it could have been claimed by another instance already. Because do_not_queue was not yet used, this means that the claim would probably not even fail, but leave a pointless process running. This commit refactors the code structure to introduce a claim_bus_name helper, which claims the name with do_not_queue=True so it either fails when the name is already taken, or is sure the name is claimed. This also raises the bus claiming code out of the Storage and WindowServer classes into the main script, since that is a better place to decide to abort startup and log messages about this. This refactoring also prepares for implementing a `--replace` option in a subsequent commit. --- src/hamster-service.py | 24 ++++++++++++------------ src/hamster-windows-service.py | 20 ++++++++++---------- src/hamster/lib/dbus.py | 14 ++++++++++++++ 3 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/hamster-service.py b/src/hamster-service.py index 1500083cd..95bc3ed91 100755 --- a/src/hamster-service.py +++ b/src/hamster-service.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 # nicked off gwibber +import sys + import dbus import dbus.service @@ -17,6 +19,7 @@ from hamster.lib import default_logger from hamster.lib.dbus import ( DBusMainLoop, + claim_bus_name, fact_signature, from_dbus_date, from_dbus_fact, @@ -33,20 +36,12 @@ DBusMainLoop(set_as_default=True) loop = glib.MainLoop() -if "org.gnome.Hamster" in dbus.SessionBus().list_names(): - print("Found hamster-service already running, exiting") - quit() - - class Storage(db.Storage, dbus.service.Object): __dbus_object_path__ = "/org/gnome/Hamster" - def __init__(self, loop): - self.bus = dbus.SessionBus() - bus_name = dbus.service.BusName("org.gnome.Hamster", bus=self.bus) - - - dbus.service.Object.__init__(self, bus_name, self.__dbus_object_path__) + def __init__(self, loop, bus, name_obj): + self.bus = bus + dbus.service.Object.__init__(self, name_obj, self.__dbus_object_path__) db.Storage.__init__(self, unsorted_localized="") self.mainloop = loop @@ -460,6 +455,11 @@ def Version(self): # hamster_logger for the rest hamster_logger.setLevel(args.log_level) - storage = Storage(loop) + (bus, name_obj) = claim_bus_name("org.gnome.Hamster") + if name_obj is None: + logger.error("Found hamster-service already running, exiting") + sys.exit(1) + + storage = Storage(loop, bus, name_obj) logger.info("hamster-service up") loop.run() diff --git a/src/hamster-windows-service.py b/src/hamster-windows-service.py index c0ad5bf7b..22be29130 100755 --- a/src/hamster-windows-service.py +++ b/src/hamster-windows-service.py @@ -5,23 +5,20 @@ import dbus.service import os.path import subprocess +import sys from dbus.mainloop.glib import DBusGMainLoop from gi.repository import GLib as glib import hamster from hamster.lib import default_logger +from hamster.lib.dbus import claim_bus_name logger = default_logger(__file__) DBusGMainLoop(set_as_default=True) loop = glib.MainLoop() -if "org.gnome.Hamster.WindowServer" in dbus.SessionBus().list_names(): - print("Found hamster-window-service already running, exiting") - quit() - - # Legacy server. Still used by the shell-extension. # New code _could_ access the org.gnome.Hamster.GUI actions directly, # although the exact action names/data are subject to change. @@ -32,12 +29,11 @@ class WindowServer(dbus.service.Object): __dbus_object_path__ = "/org/gnome/Hamster/WindowServer" - def __init__(self, loop): + def __init__(self, loop, bus, name_obj): self.app = True self.mainloop = loop - self.bus = dbus.SessionBus() - bus_name = dbus.service.BusName("org.gnome.Hamster.WindowServer", bus=self.bus) - dbus.service.Object.__init__(self, bus_name, self.__dbus_object_path__) + self.bus = bus + dbus.service.Object.__init__(self, name_obj, self.__dbus_object_path__) @dbus.service.method("org.gnome.Hamster") def Quit(self): @@ -85,7 +81,11 @@ def preferences(self): glib.set_prgname(str(_("hamster-windows-service"))) - window_server = WindowServer(loop) + (bus, name_obj) = claim_bus_name("org.gnome.Hamster.WindowServer") + if name_obj is None: + logger.error("Found hamster-windows-service already running, exiting") + sys.exit(1) + window_server = WindowServer(loop, bus, name_obj) logger.info("hamster-window-service up") loop.run() diff --git a/src/hamster/lib/dbus.py b/src/hamster/lib/dbus.py index f282e4fdc..2d290f2f1 100644 --- a/src/hamster/lib/dbus.py +++ b/src/hamster/lib/dbus.py @@ -121,3 +121,17 @@ def to_dbus_fact(fact): dbus.Array(fact.tags, signature = 's'), to_dbus_date(fact.date), fact.delta.days * 24 * 60 * 60 + fact.delta.seconds) + + +def claim_bus_name(name): + """ Claim a bus name. + + Returns (bus, name_obj), or (bus, None) when the name was already claimed. + """ + bus = dbus.SessionBus() + try: + name_obj = dbus.service.BusName(name, bus=bus, do_not_queue=True) + except dbus.exceptions.NameExistsException as e: + return (bus, None) + + return (bus, name_obj) From 3a22d3bd7b747005daf5df04df8ffa69f6364ac5 Mon Sep 17 00:00:00 2001 From: Matthijs Kooijman Date: Mon, 25 Dec 2023 21:38:30 +0100 Subject: [PATCH 06/11] Implement --replace option for all three binaries This makes them replace any currently running GUI, storage service or windows service. This is primarily useful during development, to prevent having to kill these services manually. For the two services, this is implemented properly by trying to claim the bus name and if it is taken, calling the quit method on the current instance while the existing claim is kept in the dbus request queue. This ensures that as soon as the name is released, it is claimed again by the new process (preventing a race condition where a service autostart could occur in between). For the GUI, this race condition is not prevented due to the way Gtk/Gio.Application claims its name, but this should be minor enough an issue to not be problematic (especially since the user can always easily quit the GUI manually beforehand). This commit implements most of #746. --- src/hamster-cli.py | 28 +++++++++++++++++++++++++-- src/hamster-service.py | 10 ++++++++-- src/hamster-windows-service.py | 14 ++++++++++++-- src/hamster/lib/dbus.py | 35 ++++++++++++++++++++++++++++++++-- 4 files changed, 79 insertions(+), 8 deletions(-) diff --git a/src/hamster-cli.py b/src/hamster-cli.py index 58c51c91c..97e86a198 100755 --- a/src/hamster-cli.py +++ b/src/hamster-cli.py @@ -24,6 +24,7 @@ import sys, os import argparse +import time import re import gi @@ -451,8 +452,6 @@ def version(self): """) hamster_client = HamsterCli() - app = Hamster() - logger.debug("app instanciated") import signal signal.signal(signal.SIGINT, signal.SIG_DFL) # gtk3 screws up ctrl+c @@ -467,6 +466,8 @@ def version(self): choices=('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'), default='WARNING', help="Set the logging level (default: %(default)s)") + parser.add_argument("--replace", action='store_true', + help="Replace an existing GUI process (if any) instead of activating it") parser.add_argument("action", nargs="?", default="overview") parser.add_argument('action_args', nargs=argparse.REMAINDER, default=[]) @@ -477,6 +478,29 @@ def version(self): # hamster_logger for the rest hamster_logger.setLevel(args.log_level) + app = Hamster() + logger.debug("app instantiated") + if args.replace: + app.register() + if app.get_is_remote(): + # This code is prone to race conditions (if processing the quit + # takes longer than the sleep below, or if another GUI is + # bus activated before the new app), but gio.Application + # does not offer any way to pass a pre-claimed name or dbus + # connection, and always passes DO_NOT_QUEUE when claiming + # the name, preventing properly handling this race + # condition. But it is only the GUI, so the user can always + # just manually quit any existing GUI. + logger.debug("sending quit") + app.activate_action("quit") + time.sleep(2) + app = Hamster() + logger.debug("app reinstantiated") + app.register() + if app.get_is_remote(): + logger.error("Failed to replace existing GUI") + sys.exit(1) + if not hamster.installed: logger.info("Running in devel mode") diff --git a/src/hamster-service.py b/src/hamster-service.py index 95bc3ed91..b64ead5a2 100755 --- a/src/hamster-service.py +++ b/src/hamster-service.py @@ -447,6 +447,8 @@ def Version(self): choices=('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'), default='WARNING', help="Set the logging level (default: %(default)s)") + parser.add_argument("--replace", action='store_true', + help="Replace an existing process (if any)") args = parser.parse_args() @@ -455,9 +457,13 @@ def Version(self): # hamster_logger for the rest hamster_logger.setLevel(args.log_level) - (bus, name_obj) = claim_bus_name("org.gnome.Hamster") + quit_method = (Storage.__dbus_object_path__, 'org.gnome.Hamster', 'Quit') + (bus, name_obj) = claim_bus_name("org.gnome.Hamster", quit_method=quit_method, replace=args.replace) if name_obj is None: - logger.error("Found hamster-service already running, exiting") + if args.replace: + logger.error("Failed to replace existing hamster-service (it did not quit within timeout), exiting") + else: + logger.error("Found hamster-service already running, exiting") sys.exit(1) storage = Storage(loop, bus, name_obj) diff --git a/src/hamster-windows-service.py b/src/hamster-windows-service.py index 22be29130..7fd97757b 100755 --- a/src/hamster-windows-service.py +++ b/src/hamster-windows-service.py @@ -81,9 +81,19 @@ def preferences(self): glib.set_prgname(str(_("hamster-windows-service"))) - (bus, name_obj) = claim_bus_name("org.gnome.Hamster.WindowServer") + import argparse + parser = argparse.ArgumentParser(description="Hamster time tracker D-Bus service") + parser.add_argument("--replace", action='store_true', + help="Replace an existing process (if any)") + args = parser.parse_args() + + quit_method = (WindowServer.__dbus_object_path__, 'org.gnome.Hamster', 'Quit') + (bus, name_obj) = claim_bus_name("org.gnome.Hamster", quit_method=quit_method, replace=args.replace) if name_obj is None: - logger.error("Found hamster-windows-service already running, exiting") + if args.replace: + logger.error("Failed to replace existing hamster-windows-service (it did not quit within timeout), exiting") + else: + logger.error("Found hamster-windows-service already running, exiting") sys.exit(1) window_server = WindowServer(loop, bus, name_obj) logger.info("hamster-window-service up") diff --git a/src/hamster/lib/dbus.py b/src/hamster/lib/dbus.py index 2d290f2f1..27d44acba 100644 --- a/src/hamster/lib/dbus.py +++ b/src/hamster/lib/dbus.py @@ -1,3 +1,5 @@ +import time + import dbus from dbus.mainloop.glib import DBusGMainLoop as DBusMainLoop @@ -123,15 +125,44 @@ def to_dbus_fact(fact): fact.delta.days * 24 * 60 * 60 + fact.delta.seconds) -def claim_bus_name(name): +def claim_bus_name(name, quit_method=None, quit_timeout=5, replace=False): """ Claim a bus name. Returns (bus, name_obj), or (bus, None) when the name was already claimed. + + If replace is true, quit_method should be an (object_path, + interface, method) tuple of a method to call to make the existing + name owner quit. That method is called to (atomically) replace the + existing name owner, waiting up to quit_timeout seconds for the + existing owner to quit. """ bus = dbus.SessionBus() + con = bus.get_connection() + try: - name_obj = dbus.service.BusName(name, bus=bus, do_not_queue=True) + name_obj = dbus.service.BusName(name, bus=bus, do_not_queue=not replace) except dbus.exceptions.NameExistsException as e: return (bus, None) + # If do_not_queue=False and the name is already taken, we get no + # exception or any other indication whether the name was claimed or + # queued (this is a todo in dbus-python), so we check manually by + # matching the connection names + def claimed_name(): + return bus.get_name_owner(name) == con.get_unique_name() + + if replace and not claimed_name(): + (object_path, interface_name, method_name) = quit_method + method = bus.get_object(name, object_path).get_dbus_method(method_name, dbus_interface=interface_name) + method() + + # It would be more elegant if this used NameOwnerChanged events + # rather than polling, but also more complicated, so stick to + # polling for now. + start = time.monotonic() + while not claimed_name() and time.monotonic() - start < quit_timeout: + time.sleep(0.1) + if not claimed_name(): + name_obj = None + return (bus, name_obj) From fca47a27618d48295353c9cea09fb80084067e1d Mon Sep 17 00:00:00 2001 From: Matthijs Kooijman Date: Tue, 26 Dec 2023 15:27:40 +0100 Subject: [PATCH 07/11] Remove .py extension from main scripts This makes the filenames equal to the name they get when installed, which is more consistent and makes it easier to call them programmatically (this prepares for implementing `--replace-all` in subsequent commit). The .py extension was added in commit 774c3159 (give .py extension to hamster-*), on account of letting xgettext understand these are python files. Currently, there is no working (or at least not documented) xgettext flow anyway, and xgettext seems to support passing an explicit language parameter, so this can probably be used, removing the need for these explicit extensions. --- README.md | 4 ++-- src/{hamster-cli.py => hamster-cli} | 0 src/{hamster-service.py => hamster-service} | 0 src/{hamster-windows-service.py => hamster-windows-service} | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename src/{hamster-cli.py => hamster-cli} (100%) rename src/{hamster-service.py => hamster-service} (100%) rename src/{hamster-windows-service.py => hamster-windows-service} (100%) diff --git a/README.md b/README.md index b3c116d37..36ab7c14d 100644 --- a/README.md +++ b/README.md @@ -215,8 +215,8 @@ pgrep -af hamster # and kill them one by one # or be bold and kill all processes with "hamster" in their command line pkill -ef hamster -python3 src/hamster-service.py & -python3 src/hamster-cli.py +python3 src/hamster-service & +python3 src/hamster-cli ``` Advantage: running uninstalled is detected, and windows are *not* called via D-Bus, so that all the traces are visible. diff --git a/src/hamster-cli.py b/src/hamster-cli similarity index 100% rename from src/hamster-cli.py rename to src/hamster-cli diff --git a/src/hamster-service.py b/src/hamster-service similarity index 100% rename from src/hamster-service.py rename to src/hamster-service diff --git a/src/hamster-windows-service.py b/src/hamster-windows-service similarity index 100% rename from src/hamster-windows-service.py rename to src/hamster-windows-service From eee3c1cc197d93381627fe57d2fc476035225075 Mon Sep 17 00:00:00 2001 From: Matthijs Kooijman Date: Wed, 27 Dec 2023 11:44:14 +0100 Subject: [PATCH 08/11] fixup! Implement --replace option for all three binaries --- src/hamster-windows-service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hamster-windows-service b/src/hamster-windows-service index 7fd97757b..9c3acd0a6 100755 --- a/src/hamster-windows-service +++ b/src/hamster-windows-service @@ -88,7 +88,7 @@ if __name__ == '__main__': args = parser.parse_args() quit_method = (WindowServer.__dbus_object_path__, 'org.gnome.Hamster', 'Quit') - (bus, name_obj) = claim_bus_name("org.gnome.Hamster", quit_method=quit_method, replace=args.replace) + (bus, name_obj) = claim_bus_name("org.gnome.Hamster.WindowServer", quit_method=quit_method, replace=args.replace) if name_obj is None: if args.replace: logger.error("Failed to replace existing hamster-windows-service (it did not quit within timeout), exiting") From db55ab6209b18cd85008df7e3b249e0368945e6a Mon Sep 17 00:00:00 2001 From: Matthijs Kooijman Date: Wed, 27 Dec 2023 12:01:19 +0100 Subject: [PATCH 09/11] Revert "Remove .py extension from main scripts" This reverts commit 767f80a22593757aec10b3f6ab3a4ef3ab8a0809. --- README.md | 4 ++-- src/{hamster-cli => hamster-cli.py} | 0 src/{hamster-service => hamster-service.py} | 0 src/{hamster-windows-service => hamster-windows-service.py} | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename src/{hamster-cli => hamster-cli.py} (100%) rename src/{hamster-service => hamster-service.py} (100%) rename src/{hamster-windows-service => hamster-windows-service.py} (100%) diff --git a/README.md b/README.md index 36ab7c14d..b3c116d37 100644 --- a/README.md +++ b/README.md @@ -215,8 +215,8 @@ pgrep -af hamster # and kill them one by one # or be bold and kill all processes with "hamster" in their command line pkill -ef hamster -python3 src/hamster-service & -python3 src/hamster-cli +python3 src/hamster-service.py & +python3 src/hamster-cli.py ``` Advantage: running uninstalled is detected, and windows are *not* called via D-Bus, so that all the traces are visible. diff --git a/src/hamster-cli b/src/hamster-cli.py similarity index 100% rename from src/hamster-cli rename to src/hamster-cli.py diff --git a/src/hamster-service b/src/hamster-service.py similarity index 100% rename from src/hamster-service rename to src/hamster-service.py diff --git a/src/hamster-windows-service b/src/hamster-windows-service.py similarity index 100% rename from src/hamster-windows-service rename to src/hamster-windows-service.py From e5ba311f8512df27f6e33bc490953c3db9e8812e Mon Sep 17 00:00:00 2001 From: Matthijs Kooijman Date: Wed, 27 Dec 2023 12:05:28 +0100 Subject: [PATCH 10/11] Detach/daemonize service processes This lets the service commands (hamster-service and hamster-windows-service) daemonize after initialization. This ensures they are detached from their parent process, and allows a caller to wait for them to return to know whether startup succeeded or not. --- src/hamster-service.py | 6 +++++- src/hamster-windows-service.py | 5 ++++- src/hamster/lib/stuff.py | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/hamster-service.py b/src/hamster-service.py index b64ead5a2..61a80645d 100755 --- a/src/hamster-service.py +++ b/src/hamster-service.py @@ -11,7 +11,7 @@ import hamster from hamster import logger as hamster_logger -from hamster.lib import i18n +from hamster.lib import i18n, stuff i18n.setup_i18n() # noqa: E402 from hamster.storage import db @@ -468,4 +468,8 @@ def Version(self): storage = Storage(loop, bus, name_obj) logger.info("hamster-service up") + + # Daemonize once we're succesfully started up and registered on dbus + stuff.daemonize() + loop.run() diff --git a/src/hamster-windows-service.py b/src/hamster-windows-service.py index 9c3acd0a6..ec496969a 100755 --- a/src/hamster-windows-service.py +++ b/src/hamster-windows-service.py @@ -11,7 +11,7 @@ from gi.repository import GLib as glib import hamster -from hamster.lib import default_logger +from hamster.lib import default_logger, stuff from hamster.lib.dbus import claim_bus_name logger = default_logger(__file__) @@ -98,4 +98,7 @@ def preferences(self): window_server = WindowServer(loop, bus, name_obj) logger.info("hamster-window-service up") + # Daemonize once we're succesfully started up and registered on dbus + stuff.daemonize() + loop.run() diff --git a/src/hamster/lib/stuff.py b/src/hamster/lib/stuff.py index df7cb4424..2b650be35 100644 --- a/src/hamster/lib/stuff.py +++ b/src/hamster/lib/stuff.py @@ -33,6 +33,7 @@ import re import locale import os +import sys from hamster.lib import datetime as dt @@ -261,3 +262,18 @@ def escape_pango(text): text = text.replace("<", "<") text = text.replace(">", ">") return text + +def daemonize(): + """ + Minimal daemonization by forking and letting the parent exit. + """ + pid = os.fork() + if pid == 0: + # Child, setsid to prevent getting signals sent to our parent + os.setsid() + else: + # Parent + sys.exit(0) + + # Close input, but leave output file descriptors open for logging and errors + sys.stdin.close() From 4109d79b63919ad64952450aba629dfa11738392 Mon Sep 17 00:00:00 2001 From: Matthijs Kooijman Date: Wed, 27 Dec 2023 12:21:14 +0100 Subject: [PATCH 11/11] Implement --replace-all option This replaces the GUI and two background services (by using the recently added `--replace` options). This is helpful during development to easily replace the existing deamons (even when installed system-wide) with a development version, without having to resort to fragile pkill commands. This also updates the README to recommend using this option instead of kill commands. This also removes a note about not calling windows via dbus, since that does not seem to be true with the current codebase. This fixes #746 --- README.md | 33 ++++++++++----------------------- src/hamster-cli.py | 18 +++++++++++++++++- src/hamster/client.py | 13 ++++--------- src/hamster/defs.py.in | 1 + 4 files changed, 32 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index b3c116d37..b7901f097 100644 --- a/README.md +++ b/README.md @@ -25,21 +25,16 @@ ls --reverse -clt ~/.local/share/hamster*/*.db ``` Backup the last file in the list. +### Upgrading -### Kill hamster daemons +When installed from source, it is recommended to uninstall before +installing a new version. When using system packages or flatpak, you +should be able to just install the new version. -When trying a different version, make sure to kill the running daemons: +After upgrading, the hamster background services might still be running +the old version. To replace them, you can either log out, or run: -```bash -# either step-by-step, totally safe -pkill -f hamster-service -pkill -f hamster-windows-service -# check (should be empty) -pgrep -af hamster - -# or be bold and kill them all at once: -pkill -ef hamster -``` + hamster --replace-all ### Install from packages @@ -206,22 +201,14 @@ flatpak uninstall org.gnome.Hamster #### Development During development (As explained above, backup `hamster.db` first !), -if only python files are changed +if only python files are changed (*deeper changes such as the migration to gsettings require a new install*) the changes can be quickly tested by ``` -# either -pgrep -af hamster -# and kill them one by one -# or be bold and kill all processes with "hamster" in their command line -pkill -ef hamster -python3 src/hamster-service.py & -python3 src/hamster-cli.py + ./src/hamster-cli.py --replace-all ``` -Advantage: running uninstalled is detected, and windows are *not* called via -D-Bus, so that all the traces are visible. -Note: You'll need recent version of hamster installed on your system (or +Note: You'll need recent version of hamster installed on your system (or [this workaround](https://github.com/projecthamster/hamster/issues/552#issuecomment-585166000)). #### Running tests diff --git a/src/hamster-cli.py b/src/hamster-cli.py index 97e86a198..c0e61e33c 100755 --- a/src/hamster-cli.py +++ b/src/hamster-cli.py @@ -26,6 +26,8 @@ import argparse import time import re +import pathlib +import subprocess import gi gi.require_version('Gdk', '3.0') # noqa: E402 @@ -468,6 +470,8 @@ def version(self): help="Set the logging level (default: %(default)s)") parser.add_argument("--replace", action='store_true', help="Replace an existing GUI process (if any) instead of activating it") + parser.add_argument("--replace-all", action='store_true', + help="Replace all existing hamster processes (if any)") parser.add_argument("action", nargs="?", default="overview") parser.add_argument('action_args', nargs=argparse.REMAINDER, default=[]) @@ -478,9 +482,21 @@ def version(self): # hamster_logger for the rest hamster_logger.setLevel(args.log_level) + if args.replace_all: + if hamster.installed: + from hamster import defs # only available when running installed + d = pathlib.Path(defs.LIBEXEC_DIR) + cmds = [d / 'hamster-service', d / 'hamster-windows-service'] + else: + d = pathlib.Path(__file__).parent + cmds = [d / 'hamster-service.py', d / 'hamster-windows-service.py'] + + for cmd in cmds: + subprocess.run((cmd, '--replace')) + app = Hamster() logger.debug("app instantiated") - if args.replace: + if args.replace or args.replace_all: app.register() if app.get_is_remote(): # This code is prone to race conditions (if processing the quit diff --git a/src/hamster/client.py b/src/hamster/client.py index ab3d3ba04..8d0f0071b 100644 --- a/src/hamster/client.py +++ b/src/hamster/client.py @@ -94,15 +94,10 @@ def conn(self): server: {} client: {} - This is sometimes used during bisections, - but generally calls for trouble. - - Remember to kill hamster daemons after any version change - (this is safe): - pkill -f hamster-service - pkill -f hamster-windows-service - see also: - https://github.com/projecthamster/hamster#kill-hamster-daemons + To replace the running services, you can use: + + hamster --replace-all + """.format(server_version, client_version) ) ) diff --git a/src/hamster/defs.py.in b/src/hamster/defs.py.in index c74866049..0f1f8bd44 100644 --- a/src/hamster/defs.py.in +++ b/src/hamster/defs.py.in @@ -1,2 +1,3 @@ DATA_DIR = "@DATADIR@" +LIBEXEC_DIR = "@LIBEXECDIR@" VERSION = "@VERSION@"