From a222528d004d83cd6b9495a6285ec591039b4f95 Mon Sep 17 00:00:00 2001 From: Nicolas Arenas Date: Thu, 14 May 2026 18:24:05 +0200 Subject: [PATCH 01/18] Add Jenkinsfile for devel --- Jenkins/Jenkinsfile-deb-pkg | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/Jenkins/Jenkinsfile-deb-pkg b/Jenkins/Jenkinsfile-deb-pkg index 7133403..ebdc580 100644 --- a/Jenkins/Jenkinsfile-deb-pkg +++ b/Jenkins/Jenkinsfile-deb-pkg @@ -67,6 +67,20 @@ pipeline { } } } + stage('Generate Changelog (Devel)') { + when { + branch 'devel' + } + steps { + script { + def devName = params.DEV_NAME ? params.DEV_NAME : env.DEFAULT_DEV_NAME + def devEmail = params.DEV_EMAIL ? params.DEV_EMAIL : env.DEFAULT_DEV_EMAIL + generateDebianChangelog(env.BUILD_DIR, devName, devEmail, "devel") + sh "mkdir -p ${env.BUILD_DIR}/debian/" + sh "cp ${env.BUILD_DIR}/debian/changelog ${env.BUILD_DIR}/linux/debian/" + } + } + } stage('Build') { steps { dir ("${env.BUILD_DIR}") { @@ -112,8 +126,32 @@ pipeline { } } } + + stage ('Publish to Debian Repository (Devel)') { + when { + branch 'devel' + } + agent { label 'debian-repo' } + steps { + script { + def versionPattern = "-${env.BUILD_NUMBER}~devel" + publicarEnAptly('/var/tmp/opengnsys/debian-repo/ogagent', 'devel', versionPattern) + } + } + } } post { + success { + script { + if (env.BRANCH_NAME == 'devel') { + build job: 'Aptly publish develop repository', + wait: false, + parameters: [ + string(name: 'TRIGGERED_BY', value: "${env.JOB_NAME}-${env.BUILD_NUMBER}") + ] + } + } + } always { notifyBuildStatus('narenas@qindel.com') } From 25fc2cf5f8a2bcd928feb8e24d4196a34d258ae9 Mon Sep 17 00:00:00 2001 From: Nicolas Arenas Date: Wed, 20 May 2026 14:19:08 +0200 Subject: [PATCH 02/18] feat(ogagent): select network interface by routing table instead of first found On machines with multiple NICs, the agent was always registering with ogcore using the first interface returned by the OS, which could be the wrong one if it wasn't the interface with a route to ogcore. _best_interface() opens a no-op UDP socket toward the ogcore host and reads the local IP assigned by the kernel routing table, then matches it against the available interfaces. If the routing query fails (network not ready yet) it returns None so the detection loop keeps retrying rather than accepting a wrong interface. Falls back to nets[0] only when the query succeeds but no interface matches (NAT/VPN scenario). --- .../modules/server/OpenGnSys/__init__.py | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/opengnsys/modules/server/OpenGnSys/__init__.py b/src/opengnsys/modules/server/OpenGnSys/__init__.py index 765bb5a..1203eb2 100644 --- a/src/opengnsys/modules/server/OpenGnSys/__init__.py +++ b/src/opengnsys/modules/server/OpenGnSys/__init__.py @@ -34,6 +34,7 @@ import base64 import os import random import shutil +import socket import string import sys import threading @@ -142,6 +143,36 @@ class OpenGnSysWorker(ServerWorker): logger.info('Connection to ogcore restored, re-registering') self._send_started() + def _best_interface(self, nets): + """ + Return the interface whose IP the OS routing table would use to reach + ogcore, by opening a no-op UDP socket (no packets sent) and reading + the local address assigned by the kernel. + + Returns None if the routing query itself fails (network not ready yet), + so the caller keeps retrying. Falls back to nets[0] only when the query + succeeds but no interface matches the routing IP (e.g. NAT or VPN). + """ + try: + host = urllib.parse.urlparse(self.REST.endpoint).hostname + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + s.connect((host, 80)) + local_ip = s.getsockname()[0] + except Exception as e: + logger.debug('Routing query to ogcore failed (network not ready?): {}'.format(e)) + return None + + for iface in nets: + if iface.ip == local_ip: + logger.debug('Selected interface {} ({}) for ogcore at {}'.format( + iface.name, local_ip, host)) + return iface + + # Routing succeeded but no interface matched (NAT/VPN): best effort + logger.debug('No interface matched routing IP {}, falling back to {}'.format( + local_ip, nets[0].ip)) + return nets[0] + def _setup_network(self, ca_file, crt_file, key_file): """ Background thread: waits indefinitely for a network interface, then @@ -159,8 +190,8 @@ class OpenGnSysWorker(ServerWorker): try: nets = list(operations.getNetworkInfo()) if nets: - self.interface = nets[0] - if t > 0: + self.interface = self._best_interface(nets) + if self.interface and t > 0: logger.debug("Network interface found after {} seconds".format(t)) else: logger.error('No network interfaces found') From 9ffa27cd03d23746b35f31079bf901bed79b114f Mon Sep 17 00:00:00 2001 From: Nicolas Arenas Date: Wed, 20 May 2026 14:36:34 +0200 Subject: [PATCH 03/18] HANGELOG.md --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4465ff..9353533 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Promote job completion log in `oglive_worker.mon()` from `DEBUG` to `INFO`, now including the job result. - When `interfaceAdmin()` fails, log the script stdout at `WARNING` in addition to the existing `ERROR` for stderr, providing full diagnostic context without requiring a debug-level rerun. +## [8.5.5] - 2026-05-20 + +### Changed + +- On machines with multiple network interfaces, the agent now selects the interface whose IP the OS routing table would use to reach ogcore, instead of always picking the first interface returned by the OS. This ensures the agent registers with the correct IP on multi-homed machines. If the routing query fails (network not ready yet) the detection loop keeps retrying rather than accepting a wrong interface. Falls back to the first available interface only when the routing query succeeds but no interface matches (e.g. NAT or VPN). + ## [8.5.4] - 2026-05-11 ### Fixed From 0db7dbce9f04e66a2666a5f15290011f3702cbd6 Mon Sep 17 00:00:00 2001 From: jmbardallo Date: Thu, 9 Jul 2026 11:56:53 +0200 Subject: [PATCH 04/18] refactor(deps): remove obsolete dependencies and support modern Ubuntu --- linux/debian/changelog | 7 +++++++ linux/debian/control | 4 ++-- src/opengnsys/__init__.py | 12 +----------- src/opengnsys/httpserver.py | 7 +++---- src/opengnsys/linux/operations.py | 24 ++++-------------------- src/opengnsys/macos/operations.py | 16 ---------------- src/opengnsys/utils.py | 9 ++++----- src/requirements.txt | 2 +- 8 files changed, 22 insertions(+), 59 deletions(-) diff --git a/linux/debian/changelog b/linux/debian/changelog index 7a14615..9b6dea0 100644 --- a/linux/debian/changelog +++ b/linux/debian/changelog @@ -1,3 +1,10 @@ +ogagent (8.5.5-1) stable; urgency=medium + + * Remove obsolete dependencies (six, prctl, libxss1) to support modern Ubuntu releases (like 26.04) + * Update policykit-1 to polkitd + + -- OpenGnsys developers Thu, 09 Jul 2026 12:00:00 +0200 + ogagent (8.5.4-1) stable; urgency=medium * Fix network detection race condition on slow-booting machines: activation diff --git a/linux/debian/control b/linux/debian/control index c8d9e6e..7194250 100644 --- a/linux/debian/control +++ b/linux/debian/control @@ -11,7 +11,7 @@ Section: admin Priority: optional Architecture: all Depends: - policykit-1 (>= 0.100), python3 (>=3.4) | python (>= 3.4), python3-pyqt6, python3-requests, - python3-six, python3-prctl, python3-distro, libxss1, zenity, ${misc:Depends} + polkitd | policykit-1 (>= 0.100), python3 (>=3.4), python3-pyqt6, python3-requests, + python3-distro, zenity, ${misc:Depends} Description: OpenGnsys Agent for Operating Systems This package provides the required components to allow this machine to work on an environment managed by OpenGnsys. diff --git a/src/opengnsys/__init__.py b/src/opengnsys/__init__.py index cf7a335..7f99fe5 100644 --- a/src/opengnsys/__init__.py +++ b/src/opengnsys/__init__.py @@ -31,9 +31,6 @@ """ -# On centos, old six release does not includes byte2int, nor six.PY2 -import six - from . import modules from .RESTApi import REST, RESTError @@ -46,11 +43,4 @@ __author__ = 'Adolfo Gómez' __license__ = "BSD 3-clause" __copyright__ = "Copyright VirtualCable S.L.U." -if not hasattr(six, 'byte2int'): - if six.PY3: - import operator - six.byte2int = operator.itemgetter(0) - else: - def _byte2int(bs): - return ord(bs[0]) - six.byte2int = _byte2int + diff --git a/src/opengnsys/httpserver.py b/src/opengnsys/httpserver.py index 5879618..6dc4fe9 100644 --- a/src/opengnsys/httpserver.py +++ b/src/opengnsys/httpserver.py @@ -34,10 +34,9 @@ import os import json import ssl import threading -from six.moves.socketserver import ThreadingMixIn # @UnresolvedImport -from six.moves.BaseHTTPServer import BaseHTTPRequestHandler # @UnresolvedImport -from six.moves.BaseHTTPServer import HTTPServer # @UnresolvedImport -from six.moves.urllib.parse import unquote # @UnresolvedImport +from socketserver import ThreadingMixIn +from http.server import BaseHTTPRequestHandler, HTTPServer +from urllib.parse import unquote from .utils import exceptionToMessage from .certs import createSelfSignedCert diff --git a/src/opengnsys/linux/operations.py b/src/opengnsys/linux/operations.py index 9d2d928..5af98ee 100644 --- a/src/opengnsys/linux/operations.py +++ b/src/opengnsys/linux/operations.py @@ -41,7 +41,6 @@ import ctypes.util import subprocess import struct import array -import six import distro from opengnsys import utils @@ -53,12 +52,12 @@ def _getMacAddr(ifname): ''' if isinstance(ifname, list): return dict([(name, _getMacAddr(name)) for name in ifname]) - if isinstance(ifname, six.text_type): + if isinstance(ifname, str): ifname = ifname.encode('utf-8') # If unicode, convert to bytes (or str in python 2.7) try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) info = bytearray(fcntl.ioctl(s.fileno(), 0x8927, struct.pack(str('256s'), ifname[:15]))) - return six.text_type(''.join(['%02x:' % char for char in info[18:24]])[:-1]) + return str(''.join(['%02x:' % char for char in info[18:24]])[:-1]) except Exception: return None @@ -70,11 +69,11 @@ def _getIpAddr(ifname): ''' if isinstance(ifname, list): return dict([(name, _getIpAddr(name)) for name in ifname]) - if isinstance(ifname, six.text_type): + if isinstance(ifname, str): ifname = ifname.encode('utf-8') # If unicode, convert to bytes (or str in python 2.7) try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - return six.text_type(socket.inet_ntoa(fcntl.ioctl( + return str(socket.inet_ntoa(fcntl.ioctl( s.fileno(), 0x8915, # SIOCGIFADDR struct.pack(str('256s'), ifname[:15]) @@ -138,11 +137,6 @@ def reboot(flags=0): ''' Simple reboot using os command ''' - # Workaround for dummy thread - if six.PY3 is False: - import threading - threading._DummyThread._Thread__stop = lambda x: 42 - # Check for OpenGnsys Client or GNU/Linux distribution. if os.path.exists('/scripts/oginit'): subprocess.call('source /opt/opengnsys/etc/preinit/loadenviron.sh; /opt/opengnsys/scripts/reboot', shell=True) @@ -154,11 +148,6 @@ def poweroff(flags=0): ''' Simple poweroff using os command ''' - # Workaround for dummy thread - if six.PY3 is False: - import threading - threading._DummyThread._Thread__stop = lambda x: 42 - # Check for OpenGnsys Client or GNU/Linux distribution. if os.path.exists('/scripts/oginit'): subprocess.call('source /opt/opengnsys/etc/preinit/loadenviron.sh; /opt/opengnsys/scripts/poweroff', shell=True) @@ -171,11 +160,6 @@ def logoff(): Kills all curent user processes, which must send a logogof caveat: If the user has other sessions, will also disconnect from them ''' - # Workaround for dummy thread - if six.PY3 is False: - import threading - threading._DummyThread._Thread__stop = lambda x: 42 - subprocess.call(['/usr/bin/pkill', '-u', os.environ['USER']]) diff --git a/src/opengnsys/macos/operations.py b/src/opengnsys/macos/operations.py index 5f827a1..24022d9 100644 --- a/src/opengnsys/macos/operations.py +++ b/src/opengnsys/macos/operations.py @@ -41,7 +41,6 @@ import ctypes.util import subprocess import struct import array -import six import json from opengnsys import utils @@ -134,11 +133,6 @@ def reboot(flags=0): ''' Simple reboot command ''' - # Workaround for dummy thread - if six.PY3 is False: - import threading - threading._DummyThread._Thread__stop = lambda x: 42 - # Exec reboot command subprocess.call('/sbin/shutdown -r now', shell=True) @@ -147,11 +141,6 @@ def poweroff(flags=0): ''' Simple poweroff command ''' - # Workaround for dummy thread - if six.PY3 is False: - import threading - threading._DummyThread._Thread__stop = lambda x: 42 - # Exec shutdown command subprocess.call('/sbin/shutdown -h now', shell=True) @@ -160,11 +149,6 @@ def logoff(): ''' Simple logout using AppleScript ''' - # Workaround for dummy thread - if six.PY3 is False: - import threading - threading._DummyThread._Thread__stop = lambda x: 42 - # Exec logout using AppleScript subprocess.call('/usr/bin/osascript -e \'tell app "System Events" to «event aevtrlgo»\'', shell=True) diff --git a/src/opengnsys/utils.py b/src/opengnsys/utils.py index c54b4b7..9df7a89 100644 --- a/src/opengnsys/utils.py +++ b/src/opengnsys/utils.py @@ -32,7 +32,6 @@ import sys -import six if sys.platform == 'win32': _fromEncoding = 'windows-1250' @@ -42,15 +41,15 @@ else: def toUnicode(msg): try: - if not isinstance(msg, six.text_type): - if isinstance(msg, six.binary_type): + if not isinstance(msg, str): + if isinstance(msg, bytes): return msg.decode(_fromEncoding, 'ignore') - return six.text_type(msg) + return str(msg) else: return msg except Exception: try: - return six.text_type(msg) + return str(msg) except Exception: return '' diff --git a/src/requirements.txt b/src/requirements.txt index b85c90d..29bf5af 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,6 +1,6 @@ pywin32 pyqt6 requests -six + pycryptodome pyinstaller From a149439cc2a1a517f59f9862f36c2caa8ccdc50b Mon Sep 17 00:00:00 2001 From: jmbardallo Date: Thu, 9 Jul 2026 12:15:51 +0200 Subject: [PATCH 05/18] fix(build): generate UI files with pyuic6 during build --- linux/Makefile | 1 + linux/debian/control | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/linux/Makefile b/linux/Makefile index 95d5c64..612f110 100644 --- a/linux/Makefile +++ b/linux/Makefile @@ -38,6 +38,7 @@ install-ogagent: cp $(SOURCEDIR)/OGAgentUser.py $(LIBDIR) # QT Dialogs & resources + cd $(SOURCEDIR) && python3 update.py cp $(SOURCEDIR)/*_ui.py $(LIBDIR) # Version file cp $(SOURCEDIR)/VERSION $(LIBDIR) diff --git a/linux/debian/control b/linux/debian/control index 7194250..5e8ff55 100644 --- a/linux/debian/control +++ b/linux/debian/control @@ -2,7 +2,7 @@ Source: ogagent Section: admin Priority: optional Maintainer: OpenGnsys developers -Build-Depends: debhelper (>= 7), po-debconf +Build-Depends: debhelper (>= 7), po-debconf, pyqt6-dev-tools Standards-Version: 3.9.2 Homepage: https://opengnsys.es/ From e3eb42f3b377cff4967a9a0842ca460325fdc6e9 Mon Sep 17 00:00:00 2001 From: jmbardallo Date: Thu, 9 Jul 2026 12:19:04 +0200 Subject: [PATCH 06/18] fix(build): skip gpg signature in debuild to prevent errors for end users --- linux/build-packages.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linux/build-packages.sh b/linux/build-packages.sh index baf9582..105b660 100755 --- a/linux/build-packages.sh +++ b/linux/build-packages.sh @@ -3,4 +3,4 @@ cd $(dirname "$0") # Debian based -debuild -D --build=binary --post-clean --lintian-opts --profile debian +debuild -D -us -uc --build=binary --post-clean --lintian-opts --profile debian From 158cc9ef20cd60ecd82190aeb1c96d3af7c98a2f Mon Sep 17 00:00:00 2001 From: jmbardallo Date: Thu, 9 Jul 2026 12:21:06 +0200 Subject: [PATCH 07/18] Revert "fix(build): skip gpg signature in debuild to prevent errors for end users" This reverts commit e3eb42f3b377cff4967a9a0842ca460325fdc6e9. --- linux/build-packages.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linux/build-packages.sh b/linux/build-packages.sh index 105b660..baf9582 100755 --- a/linux/build-packages.sh +++ b/linux/build-packages.sh @@ -3,4 +3,4 @@ cd $(dirname "$0") # Debian based -debuild -D -us -uc --build=binary --post-clean --lintian-opts --profile debian +debuild -D --build=binary --post-clean --lintian-opts --profile debian From d2ff2d1b35fdaa473a3fee7cbb7c97424affa1b1 Mon Sep 17 00:00:00 2001 From: jmbardallo Date: Tue, 21 Jul 2026 12:20:17 +0200 Subject: [PATCH 08/18] feat: notify ogcore when script execution finishes natively --- src/opengnsys/jobmgr.py | 2 +- .../modules/server/OpenGnSys/__init__.py | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/opengnsys/jobmgr.py b/src/opengnsys/jobmgr.py index fe5c093..e7f77e3 100644 --- a/src/opengnsys/jobmgr.py +++ b/src/opengnsys/jobmgr.py @@ -25,7 +25,7 @@ class JobMgr(): ts = now.strftime ('%Y-%m-%d %H:%M:%S.%f%z') ## '%s' doesn't work on windows job_id = hashlib.sha256 (now.isoformat().encode('UTF-8') + script.encode ('UTF-8')).hexdigest()[0:12] p = subprocess.Popen (args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - self.jobs[job_id] = { 'p': p, 'pid': p.pid, 'starttime': ts, 'script': script, 'client': is_client, 'status': 'running', 'stdout': '', 'stderr': '' } + self.jobs[job_id] = { 'p': p, 'pid': p.pid, 'starttime': ts, 'script': script, 'client': is_client, 'status': 'running', 'stdout': '', 'stderr': '', 'notified': False } self.jobs[job_id]['t1'] = threading.Thread (target=job_readstdout, args=(self.jobs[job_id],)) self.jobs[job_id]['t2'] = threading.Thread (target=job_readstderr, args=(self.jobs[job_id],)) self.jobs[job_id]['t1'].start() diff --git a/src/opengnsys/modules/server/OpenGnSys/__init__.py b/src/opengnsys/modules/server/OpenGnSys/__init__.py index 1203eb2..d730694 100644 --- a/src/opengnsys/modules/server/OpenGnSys/__init__.py +++ b/src/opengnsys/modules/server/OpenGnSys/__init__.py @@ -126,6 +126,28 @@ class OpenGnSysWorker(ServerWorker): while True: time.sleep(1) n += 1 + + # Check for finished jobs to send webhook + for k, elem in list(self.jobmgr.jobs.items()): + if elem.get('p') and elem['p'].poll() is not None: + if not elem.get('notified', False): + if not elem.get('t1').is_alive() and not elem.get('t2').is_alive(): + elem['notified'] = True + elem['status'] = 'finished' + elem['rc'] = elem['p'].returncode + + webhook_body = { + "nfn": "RUN_SCRIPT", + "job_id": k, + "res": 1 if elem['rc'] == 0 else 0, + "der": elem.get('stderr', '') + } + logger.info(f"Job {k} finished with return code {elem['rc']}. Sending webhook to ogcore.") + try: + self.REST.sendMessage('clients/status/webhook', webhook_body) + except Exception as e: + logger.error(f"Failed to send webhook for job {k}: {e}") + if not n % 10: _st_map = {'linux': 'LNX', 'macos': 'OSX', 'windows': 'WIN'} body = { From d0bde04e928b7fc0e0aca1bed775df59ab488d06 Mon Sep 17 00:00:00 2001 From: jmbardallo Date: Tue, 21 Jul 2026 12:27:56 +0200 Subject: [PATCH 09/18] chore: bump version to 8.5.6 for webhook feature --- CHANGELOG.md | 5 +++++ src/VERSION | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9353533..fcdad0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [8.5.6] - 2026-07-21 + +### Added +- Notify `ogcore` via webhook when natively executed scripts finish, bringing `ogagent` in line with `ogLive` functionality (resolves stuck traces in "Enviada" state). + ## [8.5.5] - 2026-05-13 ### Changed diff --git a/src/VERSION b/src/VERSION index 56b6be4..8405675 100644 --- a/src/VERSION +++ b/src/VERSION @@ -1 +1 @@ -8.3.1 +8.5.6 From 70f85b1fc713e12bf7f8f750e233ada21a83fc05 Mon Sep 17 00:00:00 2001 From: jmbardallo Date: Wed, 22 Jul 2026 14:19:52 +0200 Subject: [PATCH 10/18] 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] From 43c57ea32037c6e65ed4f970f4d9376a7f51be98 Mon Sep 17 00:00:00 2001 From: jmbardallo Date: Wed, 22 Jul 2026 14:37:17 +0200 Subject: [PATCH 11/18] Fix(jobmgr): send START_SCRIPT webhook when script is run under user session (client=true) --- src/opengnsys/modules/server/OpenGnSys/__init__.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/opengnsys/modules/server/OpenGnSys/__init__.py b/src/opengnsys/modules/server/OpenGnSys/__init__.py index abaec46..ac3baaa 100644 --- a/src/opengnsys/modules/server/OpenGnSys/__init__.py +++ b/src/opengnsys/modules/server/OpenGnSys/__init__.py @@ -507,10 +507,20 @@ class OpenGnSysWorker(ServerWorker): def process_client_script_launched(self, data): import tempfile + job_id = data['job_id'] 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']) + fd.write (job_id) + self.client_jobs.add(job_id) + + 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 client job {job_id}: {e}") + return True def process_client_script_finished(self, data): From 5b286e005c1159aa55bc5f6765f7bfa409f4adfb Mon Sep 17 00:00:00 2001 From: jmbardallo Date: Mon, 27 Jul 2026 18:19:34 +0200 Subject: [PATCH 12/18] Fix(ogagent): preserve PowerShell exit code when redirecting output --- CHANGELOG.md | 8 +++++++- src/opengnsys/windows/operations.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcdad0e..969c800 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [8.5.6] - 2026-07-21 +## [8.5.9] - 2026-07-23 +### Fixed +- Preserve original exit code in Windows PowerShell script wrapper when redirecting streams, ensuring ogcore receives failure status (e.g., CommandNotFoundException). +- Prevent raw PowerShell CLIXML output from leaking into stderr on Windows when a command fails non-interactively. +- Send `START_SCRIPT` webhook when a script is picked up by the agent, ensuring the UI correctly displays the "In Progress" trace state. Applies to both admin (`client=false`) and user-session (`client=true`) execution paths. + +## [8.5.6] - 2026-07-21 ### Added - Notify `ogcore` via webhook when natively executed scripts finish, bringing `ogagent` in line with `ogLive` functionality (resolves stuck traces in "Enviada" state). diff --git a/src/opengnsys/windows/operations.py b/src/opengnsys/windows/operations.py index cc2aef7..ff1e79d 100644 --- a/src/opengnsys/windows/operations.py +++ b/src/opengnsys/windows/operations.py @@ -152,7 +152,7 @@ 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" + script = f"$ProgressPreference = 'SilentlyContinue'; try {{ $errBefore = $Error.Count; $output = & {{ {script} }} 2>&1; $errAfter = $Error.Count; $exitcode = $LASTEXITCODE; $output | Out-String; if ($exitcode) {{ exit $exitcode }} elseif ($errAfter -gt $errBefore) {{ exit 1 }} }} catch {{ $_ | Out-String; exit 1 }}" 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] From 88297c60c32f662373d6eda140b2d5dd4c8d00df Mon Sep 17 00:00:00 2001 From: jmbardallo Date: Mon, 27 Jul 2026 18:52:00 +0200 Subject: [PATCH 13/18] Fix(ogagent): prevent duplicate trace creation by removing START_SCRIPT webhook for synchronous execution paths --- src/opengnsys/modules/server/OpenGnSys/__init__.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/opengnsys/modules/server/OpenGnSys/__init__.py b/src/opengnsys/modules/server/OpenGnSys/__init__.py index ac3baaa..07c762a 100644 --- a/src/opengnsys/modules/server/OpenGnSys/__init__.py +++ b/src/opengnsys/modules/server/OpenGnSys/__init__.py @@ -472,13 +472,6 @@ 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' From 120140365d2c39dcdb5a19dc477a741cd87ba168 Mon Sep 17 00:00:00 2001 From: jmbardallo Date: Mon, 27 Jul 2026 18:58:53 +0200 Subject: [PATCH 14/18] Fix(ogagent): completely remove START_SCRIPT webhook to avoid trace duplication race conditions in client paths --- src/opengnsys/modules/server/OpenGnSys/__init__.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/opengnsys/modules/server/OpenGnSys/__init__.py b/src/opengnsys/modules/server/OpenGnSys/__init__.py index 07c762a..d66b03f 100644 --- a/src/opengnsys/modules/server/OpenGnSys/__init__.py +++ b/src/opengnsys/modules/server/OpenGnSys/__init__.py @@ -505,14 +505,6 @@ class OpenGnSysWorker(ServerWorker): with open (job_id_path, 'w') as fd: fd.write (job_id) self.client_jobs.add(job_id) - - 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 client job {job_id}: {e}") return True From d35e63e60be598276c5fc0a1f02514a62725204a Mon Sep 17 00:00:00 2001 From: jmbardallo Date: Tue, 28 Jul 2026 00:34:17 +0200 Subject: [PATCH 15/18] Docs: update CHANGELOG for START_SCRIPT webhook removal --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 969c800..ee0b0da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Preserve original exit code in Windows PowerShell script wrapper when redirecting streams, ensuring ogcore receives failure status (e.g., CommandNotFoundException). - Prevent raw PowerShell CLIXML output from leaking into stderr on Windows when a command fails non-interactively. -- Send `START_SCRIPT` webhook when a script is picked up by the agent, ensuring the UI correctly displays the "In Progress" trace state. Applies to both admin (`client=false`) and user-session (`client=true`) execution paths. +- Removed the `START_SCRIPT` webhook entirely from the script launching process for both admin (`client=false`) and user-session (`client=true`) paths. This eliminates the race condition that caused duplicate traces in `ogcore`, as `ogcore` now marks traces as `IN_PROGRESS` synchronously. ## [8.5.6] - 2026-07-21 ### Added From b880f29efe09e33d7dda89440099f7ff49014643 Mon Sep 17 00:00:00 2001 From: jmbardallo Date: Wed, 29 Jul 2026 13:43:36 +0200 Subject: [PATCH 16/18] Fix(build): add -us -uc to debuild to prevent GPG signing errors --- linux/build-packages.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linux/build-packages.sh b/linux/build-packages.sh index baf9582..105b660 100755 --- a/linux/build-packages.sh +++ b/linux/build-packages.sh @@ -3,4 +3,4 @@ cd $(dirname "$0") # Debian based -debuild -D --build=binary --post-clean --lintian-opts --profile debian +debuild -D -us -uc --build=binary --post-clean --lintian-opts --profile debian From ada201fa85614c830d947ffa47d250508e9dd040 Mon Sep 17 00:00:00 2001 From: jmbardallo Date: Wed, 29 Jul 2026 13:49:08 +0200 Subject: [PATCH 17/18] Revert: Fix(build): add -us -uc to debuild to prevent GPG signing errors --- linux/build-packages.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linux/build-packages.sh b/linux/build-packages.sh index 105b660..baf9582 100755 --- a/linux/build-packages.sh +++ b/linux/build-packages.sh @@ -3,4 +3,4 @@ cd $(dirname "$0") # Debian based -debuild -D -us -uc --build=binary --post-clean --lintian-opts --profile debian +debuild -D --build=binary --post-clean --lintian-opts --profile debian From ce9490b46ef3f0bbebc020397124a20fc4d66b18 Mon Sep 17 00:00:00 2001 From: jmbardallo Date: Wed, 29 Jul 2026 13:56:34 +0200 Subject: [PATCH 18/18] Docs(build): bump debian version to 8.5.9-1 and include cross-platform fixes --- linux/debian/changelog | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/linux/debian/changelog b/linux/debian/changelog index 9b6dea0..540f6cf 100644 --- a/linux/debian/changelog +++ b/linux/debian/changelog @@ -1,3 +1,10 @@ +ogagent (8.5.9-1) stable; urgency=medium + + * Removed START_SCRIPT webhook to fix race conditions causing duplicate traces + * Added script completion webhook to resolve stuck traces in Enviada state + + -- OpenGnsys developers Wed, 29 Jul 2026 13:58:00 +0200 + ogagent (8.5.5-1) stable; urgency=medium * Remove obsolete dependencies (six, prctl, libxss1) to support modern Ubuntu releases (like 26.04)