From 70f85b1fc713e12bf7f8f750e233ada21a83fc05 Mon Sep 17 00:00:00 2001 From: jmbardallo Date: Wed, 22 Jul 2026 14:19:52 +0200 Subject: [PATCH] Feat(jobmgr): send START_SCRIPT webhook to mark trace as in_progress ; Fix(windows): prevent CLIXML output on errors by piping to Out-String --- src/VERSION | 2 +- src/about-dialog.ui | 2 +- src/opengnsys/__init__.py | 2 +- .../modules/client/OpenGnSys/__init__.py | 29 ++++++++++++ .../modules/server/OpenGnSys/__init__.py | 44 +++++++++++++++++-- src/opengnsys/windows/operations.py | 2 + 6 files changed, 75 insertions(+), 6 deletions(-) diff --git a/src/VERSION b/src/VERSION index 8405675..507ae5a 100644 --- a/src/VERSION +++ b/src/VERSION @@ -1 +1 @@ -8.5.6 +8.5.9 diff --git a/src/about-dialog.ui b/src/about-dialog.ui index 72d248c..f94b1a9 100644 --- a/src/about-dialog.ui +++ b/src/about-dialog.ui @@ -65,7 +65,7 @@ - Version 0 + Version 8.5.6 diff --git a/src/opengnsys/__init__.py b/src/opengnsys/__init__.py index 7f99fe5..8b72c2b 100644 --- a/src/opengnsys/__init__.py +++ b/src/opengnsys/__init__.py @@ -34,7 +34,7 @@ from . import modules from .RESTApi import REST, RESTError -VERSION='0' +VERSION='8.5.6' __title__ = 'OpenGnsys Agent' __version__ = VERSION diff --git a/src/opengnsys/modules/client/OpenGnSys/__init__.py b/src/opengnsys/modules/client/OpenGnSys/__init__.py index d78aeb3..c0ffdf9 100644 --- a/src/opengnsys/modules/client/OpenGnSys/__init__.py +++ b/src/opengnsys/modules/client/OpenGnSys/__init__.py @@ -53,6 +53,35 @@ class OpenGnSysWorker(ClientWorker): job_id = self.jobmgr.launch_job (script, True) self.sendServerMessage('script_launched', {'op': 'launched', 'job_id': job_id}) + # Monitor the job in the background and notify the server when done + def monitor_job(): + import time + import threading + + job = self.jobmgr.jobs.get(job_id) + if not job: + return + + p = job.get('p') + if not p: + return + + p.wait() + # Wait for stdout/stderr threads to finish + if job.get('t1'): job['t1'].join() + if job.get('t2'): job['t2'].join() + + logger.debug(f'Client job {job_id} finished, notifying server.') + self.sendServerMessage('script_finished', { + 'job_id': job_id, + 'rc': p.returncode, + 'stderr': job.get('stderr', ''), + 'stdout': job.get('stdout', '') + }) + + import threading + threading.Thread(target=monitor_job, daemon=True).start() + def process_terminatescript(self, json_params): job_id = json_params['job_id'] logger.debug('Processing terminatescript request, job_id "{}"'.format (job_id)) diff --git a/src/opengnsys/modules/server/OpenGnSys/__init__.py b/src/opengnsys/modules/server/OpenGnSys/__init__.py index d730694..abaec46 100644 --- a/src/opengnsys/modules/server/OpenGnSys/__init__.py +++ b/src/opengnsys/modules/server/OpenGnSys/__init__.py @@ -103,6 +103,7 @@ class OpenGnSysWorker(ServerWorker): exec_level = None # Execution level (permitted operations) jobmgr = JobMgr() _registered = False # Whether ogcore has acknowledged our ogagent/started + client_jobs = set() # Track active client job IDs for busy status def _send_started(self): """Send ogagent/started to ogcore and mark as registered on success.""" @@ -136,11 +137,14 @@ class OpenGnSysWorker(ServerWorker): elem['status'] = 'finished' elem['rc'] = elem['p'].returncode + stdout = elem.get('stdout', '') + stderr = elem.get('stderr', '') + der = stdout + ('\n' if stdout and stderr else '') + stderr webhook_body = { - "nfn": "RUN_SCRIPT", + "nfn": "RESPUESTA_EjecutarScript", "job_id": k, "res": 1 if elem['rc'] == 0 else 0, - "der": elem.get('stderr', '') + "der": der } logger.info(f"Job {k} finished with return code {elem['rc']}. Sending webhook to ogcore.") try: @@ -150,11 +154,13 @@ class OpenGnSysWorker(ServerWorker): if not n % 10: _st_map = {'linux': 'LNX', 'macos': 'OSX', 'windows': 'WIN'} + is_busy = any(j['p'].poll() is None for j in self.jobmgr.jobs.values()) or len(self.client_jobs) > 0 body = { "iph": self.interface.ip, "timestamp": int(time.time()), - "busy": any(j['p'].poll() is None for j in self.jobmgr.jobs.values()), + "busy": is_busy, "st": _st_map.get(operations.os_type.lower(), 'UNK'), + "loggedin": len(self.user) > 0, } result = self.REST.sendMessage('clients/status/webhook', body) if result is None: @@ -466,6 +472,13 @@ class OpenGnSysWorker(ServerWorker): if post_params.get('client', 'false') == 'false': job_id = self.jobmgr.launch_job (script, False) + try: + self.REST.sendMessage('clients/status/webhook', { + "nfn": "START_SCRIPT", + "job_id": job_id + }) + except Exception as e: + logger.error(f"Failed to send START_SCRIPT webhook for job {job_id}: {e}") return {'op': 'launched', 'job_id': job_id} else: ## post_params.get('client') is not 'false' @@ -497,8 +510,33 @@ class OpenGnSysWorker(ServerWorker): job_id_path = os.path.join(tempfile.gettempdir(), 'EjecutarScript-jobid') with open (job_id_path, 'w') as fd: fd.write (data['job_id']) + self.client_jobs.add(data['job_id']) return True + def process_client_script_finished(self, data): + job_id = data['job_id'] + rc = data['rc'] + stdout = data.get('stdout', '') + stderr = data.get('stderr', '') + der = stdout + ('\n' if stdout and stderr else '') + stderr + + if job_id in self.client_jobs: + self.client_jobs.remove(job_id) + + logger.info(f"Client job {job_id} finished with return code {rc}. Sending webhook to ogcore.") + webhook_body = { + "nfn": "RESPUESTA_EjecutarScript", + "job_id": job_id, + "res": 1 if rc == 0 else 0, + "der": der + } + try: + self.REST.sendMessage('clients/status/webhook', webhook_body) + except Exception as e: + logger.error(f"Failed to send webhook for client job {job_id}: {e}") + return True + + @execution_level('full') @check_secret def process_terminatescript(self, path, get_params, post_params, server): diff --git a/src/opengnsys/windows/operations.py b/src/opengnsys/windows/operations.py index 789bbe3..cc2aef7 100644 --- a/src/opengnsys/windows/operations.py +++ b/src/opengnsys/windows/operations.py @@ -151,6 +151,8 @@ def get_etc_path(): def build_popen_args(script): ## turn the script into utf16le, then to b64 again, and feed the blob to powershell + ## wrap the script to redirect stderr and output as a simple string to avoid CLIXML wrappers in non-interactive errors + script = f"& {{ {script} }} 2>&1 | Out-String" u16 = script.encode ('utf-16le') ## utf16 b64 = base64.b64encode (u16).decode ('utf-8') ## b64 (which returns bytes, so we need an additional decode(utf8)) return ['powershell', '-WindowStyle', 'Hidden', '-EncodedCommand', b64]