From 9f7889c3e08dbc0630dd87b8da2e7e10e43f9dee Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 26 Feb 2025 16:02:57 +0100 Subject: [PATCH] sse: add the 'started_by' info to heartbeat events and support detecting that operation were started by crons --- locales/en.json | 5 ++++- src/log.py | 29 +++++++++++++++++++++++------ src/utils/sse.py | 16 +++++++++++----- 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/locales/en.json b/locales/en.json index 5a427972b3..a2f85f79e2 100644 --- a/locales/en.json +++ b/locales/en.json @@ -117,6 +117,7 @@ "ask_new_path": "New path", "ask_password": "Password", "ask_user_domain": "Domain to use for the user's email address", + "automatic_task": "Automatic task", "backup_abstract_method": "This backup method has yet to be implemented", "backup_actually_backuping": "Creating a backup archive from the collected files…", "backup_app_failed": "Could not back up {app}", @@ -683,6 +684,7 @@ "migrations_success_forward": "Migration {id} completed", "migrations_to_be_ran_manually": "Migration {id} has to be run manually. Please go to Tools → Migrations on the webadmin page, or run `yunohost tools migrations run`.", "nftables_unavailable": "You cannot play with nftables here. You are either in a container or your kernel does not support it", + "noninteractive_task": "Non-interactive task", "not_enough_disk_space": "Not enough free space on '{path}'", "operation_interrupted": "The operation was manually interrupted?", "other_available_options": "… and {n} other available options not shown", @@ -860,8 +862,9 @@ "user_updated": "User info changed", "visitors": "Visitors", "yunohost_already_installed": "YunoHost is already installed", + "yunohost_api": "YunoHost API", "yunohost_configured": "YunoHost is now configured", "yunohost_installing": "Installing YunoHost…", "yunohost_not_installed": "YunoHost is not correctly installed. Please run 'yunohost tools postinstall'", "yunohost_postinstall_end_tip": "The post-install completed! To finalize your setup, please consider:\n - diagnose potential issues through the 'Diagnosis' section of the webadmin (or 'yunohost diagnosis run' in command-line);\n - reading the 'Finalizing your setup' and 'Getting to know YunoHost' parts in the admin documentation: https://yunohost.org/admindoc." -} +} \ No newline at end of file diff --git a/src/log.py b/src/log.py index a4e02146b9..f7d6692201 100755 --- a/src/log.py +++ b/src/log.py @@ -584,7 +584,6 @@ def identify_data_to_redact(self, record): "Failed to parse line to try to identify data to redact ... : %s" % e ) - class OperationLogger: """ Instances of this class represents unit operation done on the ynh instance. @@ -631,12 +630,8 @@ def __init__( except Exception: # During postinstall, we're not actually authenticated so eeeh what happens exactly? self.started_by = "root" - elif "SUDO_USER" in os.environ: - self.started_by = os.environ["SUDO_USER"] - elif not os.isatty(1): - self.started_by = "noninteractive" else: - self.started_by = "root" + self.started_by = _guess_who_started_process(psutil.Process()) if not os.path.exists(OPERATIONS_PATH): os.makedirs(OPERATIONS_PATH) @@ -1002,3 +997,25 @@ def _get_description_from_name(name): @is_unit_operation(flash=True) def log_share(path): return log_show(path, share=True) + + +def _guess_who_started_process(process): + + if 'SUDO_USER' in process.environ(): + return process.environ()['SUDO_USER'] + + parent = process.parent() + parent_cli = parent.cmdline() + pparent = parent.parent() if parent else None + pparent_cli = pparent.cmdline() if pparent else [] + ppparent = pparent.parent() if pparent else None + ppparent_cli = ppparent.cmdline() if ppparent else [] + + if any("/usr/sbin/CRON" in cli for cli in [parent_cli, pparent_cli, ppparent_cli]): + return m18n.n("automatic_task") + elif any("/usr/bin/yunohost-api" in cli for cli in [parent_cli, pparent_cli, ppparent_cli]): + return m18n.n("yunohost_api") + elif process.terminal() is None: + return m18n.n("noninteractive_task") + else: + return "root" \ No newline at end of file diff --git a/src/utils/sse.py b/src/utils/sse.py index c03662cdcf..557ccc3a32 100644 --- a/src/utils/sse.py +++ b/src/utils/sse.py @@ -155,12 +155,15 @@ def close(self, *args, **kwargs): def get_current_operation(): + + from yunohost.log import _guess_who_started_process + try: with open("/var/run/moulinette_yunohost.lock") as f: pid = f.read().strip().split("\n")[0] lock_mtime = os.path.getmtime("/var/run/moulinette_yunohost.lock") except FileNotFoundError: - return None, None, None + return None, None, None, None try: process = psutil.Process(int(pid)) @@ -169,7 +172,7 @@ def get_current_operation(): " ".join(process.cmdline()[1:]).replace("/usr/bin/", "") or "???" ) except Exception: - return None, None, None + return None, None, None, None active_logs = [ p.path.split("/")[-1] @@ -183,7 +186,9 @@ def get_current_operation(): else: operation_id = f"lock-{lock_mtime}" - return pid, operation_id, process_command_line + started_by = _guess_who_started_process(process) + + return pid, operation_id, process_command_line, started_by def sse_stream(): @@ -201,7 +206,7 @@ def sse_stream(): yield "retry: 100\n\n" # Check if there's any ongoing operation right now - _, current_operation_id, _ = get_current_operation() + _, current_operation_id, _, _ = get_current_operation() # Log list metadata is cached so it shouldnt be a bit deal to ask for "details" (which loads the metadata yaml for every operation) recent_operation_history = log_list(since_days_ago=2, limit=20, with_details=True)[ @@ -245,11 +250,12 @@ def sse_stream(): try: while True: if time.time() - last_heartbeat > SSE_HEARTBEAT_PERIOD: - _, current_operation_id, cmdline = get_current_operation() + _, current_operation_id, cmdline, started_by = get_current_operation() data = { "current_operation": current_operation_id, "cmdline": cmdline, "timestamp": time.time(), + "started_by": started_by, } payload = json.dumps(data) yield "event: heartbeat\n"