devel 8.5.9 #76

Merged
jmbardallo merged 18 commits from devel into main 2026-07-31 10:39:29 +02:00
17 changed files with 211 additions and 68 deletions

View File

@ -5,6 +5,17 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [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.
- 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
- 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 ## [8.5.5] - 2026-05-13
### Changed ### Changed
@ -13,6 +24,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. - 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. - 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 ## [8.5.4] - 2026-05-11
### Fixed ### Fixed

View File

@ -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') { stage('Build') {
steps { steps {
dir ("${env.BUILD_DIR}") { 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 { 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 { always {
notifyBuildStatus('narenas@qindel.com') notifyBuildStatus('narenas@qindel.com')
} }

View File

@ -38,6 +38,7 @@ install-ogagent:
cp $(SOURCEDIR)/OGAgentUser.py $(LIBDIR) cp $(SOURCEDIR)/OGAgentUser.py $(LIBDIR)
# QT Dialogs & resources # QT Dialogs & resources
cd $(SOURCEDIR) && python3 update.py
cp $(SOURCEDIR)/*_ui.py $(LIBDIR) cp $(SOURCEDIR)/*_ui.py $(LIBDIR)
# Version file # Version file
cp $(SOURCEDIR)/VERSION $(LIBDIR) cp $(SOURCEDIR)/VERSION $(LIBDIR)

View File

@ -1,3 +1,17 @@
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 <info@opengnsys.es> 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)
* Update policykit-1 to polkitd
-- OpenGnsys developers <info@opengnsys.es> Thu, 09 Jul 2026 12:00:00 +0200
ogagent (8.5.4-1) stable; urgency=medium ogagent (8.5.4-1) stable; urgency=medium
* Fix network detection race condition on slow-booting machines: activation * Fix network detection race condition on slow-booting machines: activation

View File

@ -2,7 +2,7 @@ Source: ogagent
Section: admin Section: admin
Priority: optional Priority: optional
Maintainer: OpenGnsys developers <info@opengnsys.es> Maintainer: OpenGnsys developers <info@opengnsys.es>
Build-Depends: debhelper (>= 7), po-debconf Build-Depends: debhelper (>= 7), po-debconf, pyqt6-dev-tools
Standards-Version: 3.9.2 Standards-Version: 3.9.2
Homepage: https://opengnsys.es/ Homepage: https://opengnsys.es/
@ -11,7 +11,7 @@ Section: admin
Priority: optional Priority: optional
Architecture: all Architecture: all
Depends: Depends:
policykit-1 (>= 0.100), python3 (>=3.4) | python (>= 3.4), python3-pyqt6, python3-requests, polkitd | policykit-1 (>= 0.100), python3 (>=3.4), python3-pyqt6, python3-requests,
python3-six, python3-prctl, python3-distro, libxss1, zenity, ${misc:Depends} python3-distro, zenity, ${misc:Depends}
Description: OpenGnsys Agent for Operating Systems Description: OpenGnsys Agent for Operating Systems
This package provides the required components to allow this machine to work on an environment managed by OpenGnsys. This package provides the required components to allow this machine to work on an environment managed by OpenGnsys.

View File

@ -1 +1 @@
8.3.1 8.5.9

View File

@ -65,7 +65,7 @@
<item> <item>
<widget class="QLabel" name="VersionLabel"> <widget class="QLabel" name="VersionLabel">
<property name="text"> <property name="text">
<string>Version 0</string> <string>Version 8.5.6</string>
</property> </property>
</widget> </widget>
</item> </item>

View File

@ -31,13 +31,10 @@
""" """
# On centos, old six release does not includes byte2int, nor six.PY2
import six
from . import modules from . import modules
from .RESTApi import REST, RESTError from .RESTApi import REST, RESTError
VERSION='0' VERSION='8.5.6'
__title__ = 'OpenGnsys Agent' __title__ = 'OpenGnsys Agent'
__version__ = VERSION __version__ = VERSION
@ -46,11 +43,4 @@ __author__ = 'Adolfo Gómez'
__license__ = "BSD 3-clause" __license__ = "BSD 3-clause"
__copyright__ = "Copyright VirtualCable S.L.U." __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

View File

@ -34,10 +34,9 @@ import os
import json import json
import ssl import ssl
import threading import threading
from six.moves.socketserver import ThreadingMixIn # @UnresolvedImport from socketserver import ThreadingMixIn
from six.moves.BaseHTTPServer import BaseHTTPRequestHandler # @UnresolvedImport from http.server import BaseHTTPRequestHandler, HTTPServer
from six.moves.BaseHTTPServer import HTTPServer # @UnresolvedImport from urllib.parse import unquote
from six.moves.urllib.parse import unquote # @UnresolvedImport
from .utils import exceptionToMessage from .utils import exceptionToMessage
from .certs import createSelfSignedCert from .certs import createSelfSignedCert

View File

@ -25,7 +25,7 @@ class JobMgr():
ts = now.strftime ('%Y-%m-%d %H:%M:%S.%f%z') ## '%s' doesn't work on windows 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] 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) 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]['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]['t2'] = threading.Thread (target=job_readstderr, args=(self.jobs[job_id],))
self.jobs[job_id]['t1'].start() self.jobs[job_id]['t1'].start()

View File

@ -41,7 +41,6 @@ import ctypes.util
import subprocess import subprocess
import struct import struct
import array import array
import six
import distro import distro
from opengnsys import utils from opengnsys import utils
@ -53,12 +52,12 @@ def _getMacAddr(ifname):
''' '''
if isinstance(ifname, list): if isinstance(ifname, list):
return dict([(name, _getMacAddr(name)) for name in ifname]) 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) ifname = ifname.encode('utf-8') # If unicode, convert to bytes (or str in python 2.7)
try: try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
info = bytearray(fcntl.ioctl(s.fileno(), 0x8927, struct.pack(str('256s'), ifname[:15]))) 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: except Exception:
return None return None
@ -70,11 +69,11 @@ def _getIpAddr(ifname):
''' '''
if isinstance(ifname, list): if isinstance(ifname, list):
return dict([(name, _getIpAddr(name)) for name in ifname]) 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) ifname = ifname.encode('utf-8') # If unicode, convert to bytes (or str in python 2.7)
try: try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 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(), s.fileno(),
0x8915, # SIOCGIFADDR 0x8915, # SIOCGIFADDR
struct.pack(str('256s'), ifname[:15]) struct.pack(str('256s'), ifname[:15])
@ -138,11 +137,6 @@ def reboot(flags=0):
''' '''
Simple reboot using os command 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. # Check for OpenGnsys Client or GNU/Linux distribution.
if os.path.exists('/scripts/oginit'): if os.path.exists('/scripts/oginit'):
subprocess.call('source /opt/opengnsys/etc/preinit/loadenviron.sh; /opt/opengnsys/scripts/reboot', shell=True) 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 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. # Check for OpenGnsys Client or GNU/Linux distribution.
if os.path.exists('/scripts/oginit'): if os.path.exists('/scripts/oginit'):
subprocess.call('source /opt/opengnsys/etc/preinit/loadenviron.sh; /opt/opengnsys/scripts/poweroff', shell=True) 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 Kills all curent user processes, which must send a logogof
caveat: If the user has other sessions, will also disconnect from them 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']]) subprocess.call(['/usr/bin/pkill', '-u', os.environ['USER']])

View File

@ -41,7 +41,6 @@ import ctypes.util
import subprocess import subprocess
import struct import struct
import array import array
import six
import json import json
from opengnsys import utils from opengnsys import utils
@ -134,11 +133,6 @@ def reboot(flags=0):
''' '''
Simple reboot command Simple reboot command
''' '''
# Workaround for dummy thread
if six.PY3 is False:
import threading
threading._DummyThread._Thread__stop = lambda x: 42
# Exec reboot command # Exec reboot command
subprocess.call('/sbin/shutdown -r now', shell=True) subprocess.call('/sbin/shutdown -r now', shell=True)
@ -147,11 +141,6 @@ def poweroff(flags=0):
''' '''
Simple poweroff command Simple poweroff command
''' '''
# Workaround for dummy thread
if six.PY3 is False:
import threading
threading._DummyThread._Thread__stop = lambda x: 42
# Exec shutdown command # Exec shutdown command
subprocess.call('/sbin/shutdown -h now', shell=True) subprocess.call('/sbin/shutdown -h now', shell=True)
@ -160,11 +149,6 @@ def logoff():
''' '''
Simple logout using AppleScript 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 # Exec logout using AppleScript
subprocess.call('/usr/bin/osascript -e \'tell app "System Events" to «event aevtrlgo»\'', shell=True) subprocess.call('/usr/bin/osascript -e \'tell app "System Events" to «event aevtrlgo»\'', shell=True)

View File

@ -53,6 +53,35 @@ class OpenGnSysWorker(ClientWorker):
job_id = self.jobmgr.launch_job (script, True) job_id = self.jobmgr.launch_job (script, True)
self.sendServerMessage('script_launched', {'op': 'launched', 'job_id': job_id}) 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): def process_terminatescript(self, json_params):
job_id = json_params['job_id'] job_id = json_params['job_id']
logger.debug('Processing terminatescript request, job_id "{}"'.format (job_id)) logger.debug('Processing terminatescript request, job_id "{}"'.format (job_id))

View File

@ -34,6 +34,7 @@ import base64
import os import os
import random import random
import shutil import shutil
import socket
import string import string
import sys import sys
import threading import threading
@ -102,6 +103,7 @@ class OpenGnSysWorker(ServerWorker):
exec_level = None # Execution level (permitted operations) exec_level = None # Execution level (permitted operations)
jobmgr = JobMgr() jobmgr = JobMgr()
_registered = False # Whether ogcore has acknowledged our ogagent/started _registered = False # Whether ogcore has acknowledged our ogagent/started
client_jobs = set() # Track active client job IDs for busy status
def _send_started(self): def _send_started(self):
"""Send ogagent/started to ogcore and mark as registered on success.""" """Send ogagent/started to ogcore and mark as registered on success."""
@ -125,13 +127,40 @@ class OpenGnSysWorker(ServerWorker):
while True: while True:
time.sleep(1) time.sleep(1)
n += 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
stdout = elem.get('stdout', '')
stderr = elem.get('stderr', '')
der = stdout + ('\n' if stdout and stderr else '') + stderr
webhook_body = {
"nfn": "RESPUESTA_EjecutarScript",
"job_id": k,
"res": 1 if elem['rc'] == 0 else 0,
"der": der
}
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: if not n % 10:
_st_map = {'linux': 'LNX', 'macos': 'OSX', 'windows': 'WIN'} _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 = { body = {
"iph": self.interface.ip, "iph": self.interface.ip,
"timestamp": int(time.time()), "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'), "st": _st_map.get(operations.os_type.lower(), 'UNK'),
"loggedin": len(self.user) > 0,
} }
result = self.REST.sendMessage('clients/status/webhook', body) result = self.REST.sendMessage('clients/status/webhook', body)
if result is None: if result is None:
@ -142,6 +171,36 @@ class OpenGnSysWorker(ServerWorker):
logger.info('Connection to ogcore restored, re-registering') logger.info('Connection to ogcore restored, re-registering')
self._send_started() 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): def _setup_network(self, ca_file, crt_file, key_file):
""" """
Background thread: waits indefinitely for a network interface, then Background thread: waits indefinitely for a network interface, then
@ -159,8 +218,8 @@ class OpenGnSysWorker(ServerWorker):
try: try:
nets = list(operations.getNetworkInfo()) nets = list(operations.getNetworkInfo())
if nets: if nets:
self.interface = nets[0] self.interface = self._best_interface(nets)
if t > 0: if self.interface and t > 0:
logger.debug("Network interface found after {} seconds".format(t)) logger.debug("Network interface found after {} seconds".format(t))
else: else:
logger.error('No network interfaces found') logger.error('No network interfaces found')
@ -441,11 +500,38 @@ class OpenGnSysWorker(ServerWorker):
def process_client_script_launched(self, data): def process_client_script_launched(self, data):
import tempfile import tempfile
job_id = data['job_id']
job_id_path = os.path.join(tempfile.gettempdir(), 'EjecutarScript-jobid') job_id_path = os.path.join(tempfile.gettempdir(), 'EjecutarScript-jobid')
with open (job_id_path, 'w') as fd: with open (job_id_path, 'w') as fd:
fd.write (data['job_id']) fd.write (job_id)
self.client_jobs.add(job_id)
return True 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') @execution_level('full')
@check_secret @check_secret
def process_terminatescript(self, path, get_params, post_params, server): def process_terminatescript(self, path, get_params, post_params, server):

View File

@ -32,7 +32,6 @@
import sys import sys
import six
if sys.platform == 'win32': if sys.platform == 'win32':
_fromEncoding = 'windows-1250' _fromEncoding = 'windows-1250'
@ -42,15 +41,15 @@ else:
def toUnicode(msg): def toUnicode(msg):
try: try:
if not isinstance(msg, six.text_type): if not isinstance(msg, str):
if isinstance(msg, six.binary_type): if isinstance(msg, bytes):
return msg.decode(_fromEncoding, 'ignore') return msg.decode(_fromEncoding, 'ignore')
return six.text_type(msg) return str(msg)
else: else:
return msg return msg
except Exception: except Exception:
try: try:
return six.text_type(msg) return str(msg)
except Exception: except Exception:
return '' return ''

View File

@ -151,6 +151,8 @@ def get_etc_path():
def build_popen_args(script): def build_popen_args(script):
## turn the script into utf16le, then to b64 again, and feed the blob to powershell ## 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"$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 u16 = script.encode ('utf-16le') ## utf16
b64 = base64.b64encode (u16).decode ('utf-8') ## b64 (which returns bytes, so we need an additional decode(utf8)) b64 = base64.b64encode (u16).decode ('utf-8') ## b64 (which returns bytes, so we need an additional decode(utf8))
return ['powershell', '-WindowStyle', 'Hidden', '-EncodedCommand', b64] return ['powershell', '-WindowStyle', 'Hidden', '-EncodedCommand', b64]

View File

@ -1,6 +1,6 @@
pywin32 pywin32
pyqt6 pyqt6
requests requests
six
pycryptodome pycryptodome
pyinstaller pyinstaller