From 3910a842d4794dd91e2d8c3f38b504c70e378424 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20M=2E=20G=C3=B3mez?= Date: Wed, 9 Sep 2020 14:33:55 +0200 Subject: [PATCH] #940: Build an OGAgent for Windows Python 2-compatible. --- src/OGAgentUser-qt4.py | 351 +++++++++++++++++++++++++++++++++++++++++ src/setup.py | 8 +- src/update.sh | 8 + windows/build.bat | 1 + 4 files changed, 364 insertions(+), 4 deletions(-) create mode 100644 src/OGAgentUser-qt4.py diff --git a/src/OGAgentUser-qt4.py b/src/OGAgentUser-qt4.py new file mode 100644 index 0000000..90c92d6 --- /dev/null +++ b/src/OGAgentUser-qt4.py @@ -0,0 +1,351 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" +@author: Adolfo Gómez, dkmaster at dkmon dot com +""" +from __future__ import unicode_literals + +import sys +import time +import json +import six +import atexit +from PyQt4 import QtCore, QtGui # @UnresolvedImport + +from opengnsys import VERSION, ipc, operations, utils +from opengnsys.log import logger +from opengnsys.service import IPC_PORT +from about_dialog_qt4_ui import Ui_OGAAboutDialog +from message_dialog_qt4_ui import Ui_OGAMessageDialog +from opengnsys.scriptThread import ScriptExecutorThread +from opengnsys.config import readConfig +from opengnsys.loader import loadModules + +# Set default characters encoding to UTF-8 +reload(sys) +if hasattr(sys, 'setdefaultencoding'): + sys.setdefaultencoding('utf-8') + +trayIcon = None + + +def sigAtExit(): + if trayIcon: + trayIcon.quit() + + +# About dialog +class OGAAboutDialog(QtGui.QDialog): + def __init__(self, parent=None): + QtGui.QDialog.__init__(self, parent) + self.ui = Ui_OGAAboutDialog() + self.ui.setupUi(self) + self.ui.VersionLabel.setText("Version " + VERSION) + + def closeDialog(self): + self.hide() + + +class OGAMessageDialog(QtGui.QDialog): + def __init__(self, parent=None): + QtGui.QDialog.__init__(self, parent) + self.ui = Ui_OGAMessageDialog() + self.ui.setupUi(self) + + def message(self, message): + self.ui.message.setText(message) + self.show() + + def closeDialog(self): + self.hide() + + +class MessagesProcessor(QtCore.QThread): + logoff = QtCore.pyqtSignal(name='logoff') + message = QtCore.pyqtSignal(tuple, name='message') + script = QtCore.pyqtSignal(QtCore.QString, name='script') + exit = QtCore.pyqtSignal(name='exit') + + def __init__(self, port): + super(self.__class__, self).__init__() + # Retries connection for a while + for _ in range(10): + try: + self.ipc = ipc.ClientIPC(port) + self.ipc.start() + break + except Exception: + logger.debug('IPC Server is not reachable') + self.ipc = None + time.sleep(2) + + self.running = False + + def stop(self): + self.running = False + if self.ipc: + self.ipc.stop() + + def isAlive(self): + return self.ipc is not None + + def sendLogin(self, user_data): + if self.ipc: + self.ipc.sendLogin(user_data) + + def sendLogout(self, userName): + if self.ipc: + self.ipc.sendLogout(userName) + + def run(self): + if self.ipc is None: + return + self.running = True + + # Wait a bit so we ensure IPC thread is running... + time.sleep(2) + + while self.running and self.ipc.running: + try: + msg = self.ipc.getMessage() + if msg is None: + break + msg_id, data = msg + logger.debug('Got Message on User Space: {}:{}'.format(msg_id, data)) + if msg_id == ipc.MSG_MESSAGE: + module, message, data = data.split('\0') + self.message.emit((module, message, data)) + elif msg_id == ipc.MSG_LOGOFF: + self.logoff.emit() + elif msg_id == ipc.MSG_SCRIPT: + self.script.emit(QtCore.QString.fromUtf8(data)) + except Exception as e: + try: + logger.error('Got error on IPC thread {}'.format(utils.exceptionToMessage(e))) + except: + logger.error('Got error on IPC thread (an unicode error??)') + + if self.ipc.running is False and self.running is True: + logger.warn('Lost connection with Service, closing program') + + self.exit.emit() + + +class OGASystemTray(QtGui.QSystemTrayIcon): + def __init__(self, app_, parent=None): + self.app = app_ + self.config = readConfig(client=True) + self.modules = None + + # Get opengnsys section as dict + cfg = dict(self.config.items('opengnsys')) + + # Set up log level + logger.setLevel(cfg.get('log', 'INFO')) + + self.ipcport = int(cfg.get('ipc_port', IPC_PORT)) + + # style = app.style() + # icon = QtGui.QIcon(style.standardPixmap(QtGui.QStyle.SP_ComputerIcon)) + icon = QtGui.QIcon(':/images/img/oga.png') + + QtGui.QSystemTrayIcon.__init__(self, icon, parent) + self.menu = QtGui.QMenu(parent) + exit_action = self.menu.addAction("About") + exit_action.triggered.connect(self.about) + self.setContextMenu(self.menu) + self.ipc = MessagesProcessor(self.ipcport) + + if self.ipc.isAlive() is False: + raise Exception('No connection to service, exiting.') + + self.timer = QtCore.QTimer() + self.timer.timeout.connect(self.timerFnc) + + self.stopped = False + + self.ipc.message.connect(self.message) + self.ipc.exit.connect(self.quit) + self.ipc.script.connect(self.executeScript) + self.ipc.logoff.connect(self.logoff) + + self.aboutDlg = OGAAboutDialog() + self.msgDlg = OGAMessageDialog() + + self.timer.start(1000) # Launch idle checking every 1 seconds + + self.ipc.start() + + def initialize(self): + # Load modules and activate them + # Also, sends "login" event to service + self.modules = loadModules(self, client=True) + logger.debug('Modules: {}'.format(list(v.name for v in self.modules))) + + # Send init to all modules + valid_mods = [] + for mod in self.modules: + try: + logger.debug('Activating module {}'.format(mod.name)) + mod.activate() + valid_mods.append(mod) + except Exception as e: + logger.exception() + logger.error("Activation of {} failed: {}".format(mod.name, utils.exceptionToMessage(e))) + + self.modules[:] = valid_mods # copy instead of assignment + + # If this is running, it's because he have logged in, inform service of this fact + self.ipc.sendLogin((operations.getCurrentUser(), operations.getSessionLanguage(), + operations.get_session_type())) + + def deinitialize(self): + for mod in reversed(self.modules): # Deinitialize reversed of initialization + try: + logger.debug('Deactivating module {}'.format(mod.name)) + mod.deactivate() + except Exception as e: + logger.exception() + logger.error("Deactivation of {} failed: {}".format(mod.name, utils.exceptionToMessage(e))) + + def timerFnc(self): + pass + + def message(self, msg): + """ + Processes the message sent asynchronously, msg is an QString + """ + try: + logger.debug('msg: {}, {}'.format(type(msg), msg)) + module, message, data = msg + except Exception as e: + logger.error('Got exception {} processing message {}'.format(e, msg)) + return + + for v in self.modules: + if v.name == module: # Case Sensitive!!!! + try: + logger.debug('Notifying message {} to module {} with json data {}'.format(message, v.name, data)) + v.processMessage(message, json.loads(data)) + return + except Exception as e: + logger.error('Got exception {} processing generic message on {}'.format(e, v.name)) + + logger.error('Module {} not found, messsage {} not sent'.format(module, message)) + + def executeScript(self, script): + logger.debug('Executing script') + script = six.text_type(script.toUtf8()).decode('base64') + th = ScriptExecutorThread(script) + th.start() + + def logoff(self): + logger.debug('Logoff invoked') + operations.logoff() # Invoke log off + + def about(self): + self.aboutDlg.exec_() + + def cleanup(self): + logger.debug('Quit invoked') + if self.stopped is False: + self.stopped = True + try: + self.deinitialize() + except Exception: + logger.exception() + logger.error('Got exception deinitializing modules') + + try: + # If we close Client, send Logoff to Broker + self.ipc.sendLogout(operations.getCurrentUser()) + time.sleep(1) + self.timer.stop() + self.ipc.stop() + except Exception: + # May we have lost connection with server, simply log and exit in that case + logger.exception() + logger.exception("Got an exception, processing quit") + + try: + # operations.logoff() # Uncomment this after testing to logoff user + pass + except Exception: + pass + + def quit(self): + # logger.debug("Exec quit {}".format(self.stopped)) + if self.stopped is False: + self.cleanup() + self.app.quit() + + def closeEvent(self, event): + logger.debug("Exec closeEvent") + event.accept() + self.quit() + + +if __name__ == '__main__': + app = QtGui.QApplication(sys.argv) + + if not QtGui.QSystemTrayIcon.isSystemTrayAvailable(): + # QtGui.QMessageBox.critical(None, "Systray", "I couldn't detect any system tray on this system.") + sys.exit(1) + + # This is important so our app won't close on messages windows (alerts, etc...) + QtGui.QApplication.setQuitOnLastWindowClosed(False) + + try: + trayIcon = OGASystemTray(app) + except Exception as e: + logger.exception() + logger.error('OGA Service is not running, or it can\'t contact with OGA Server. User Tools stopped: {}'.format( + utils.exceptionToMessage(e))) + sys.exit(1) + + try: + trayIcon.initialize() # Initialize modules, etc.. + except Exception as e: + logger.exception() + logger.error('Exception initializing OpenGnsys User Agent {}'.format(utils.exceptionToMessage(e))) + trayIcon.quit() + sys.exit(1) + + app.aboutToQuit.connect(trayIcon.cleanup) + trayIcon.show() + + # Catch kill and logout user :) + atexit.register(sigAtExit) + + res = app.exec_() + + logger.debug('Exiting') + trayIcon.quit() + + sys.exit(res) diff --git a/src/setup.py b/src/setup.py index 7f5c04f..9a014c9 100644 --- a/src/setup.py +++ b/src/setup.py @@ -65,7 +65,7 @@ try: with open('VERSION', 'r') as v: VERSION = v.read().rstrip() except IOError: - VERSION = '1.1.0' + VERSION = '1.2.0' sys.argv.append('py2exe') @@ -99,7 +99,7 @@ class Target: # thus don't need to run in a console. -udsservice = Target( +ogaservice = Target( description='OpenGnsys Agent Service', modules=['opengnsys.windows.OGAgentService'], icon_resources=[(0, 'img\\oga.ico'), (1, 'img\\oga.ico')], @@ -112,7 +112,7 @@ HIDDEN_BY_SIX = ['SocketServer', 'SimpleHTTPServer', 'urllib'] setup( windows=[ { - 'script': 'OGAgentUser.py', + 'script': 'OGAgentUser-qt4.py', 'icon_resources': [(0, 'img\\oga.ico'), (1, 'img\\oga.ico')] }, ], @@ -121,7 +121,7 @@ setup( 'script': 'OGAServiceHelper.py' } ], - service=[udsservice], + service=[ogaservice], data_files=[('', [get_requests_cert_file()]), ('cfg', ['cfg/ogagent.cfg', 'cfg/ogclient.cfg'])], options={ 'py2exe': { diff --git a/src/update.sh b/src/update.sh index f1db2ad..29bf614 100755 --- a/src/update.sh +++ b/src/update.sh @@ -30,13 +30,21 @@ function process { pyuic5 about-dialog.ui -o about_dialog_ui.py -x pyuic5 message-dialog.ui -o message_dialog_ui.py -x +} + +function process_qt4 { + sed 's/OGAgent.qrc/OGAgent_qt4.qrc/' about-dialog.ui > about-dialog-qt4.ui + pyuic4 about-dialog-qt4.ui -o about_dialog_qt4_ui.py -x + pyuic4 message-dialog.ui -o message_dialog_qt4_ui.py -x } cd $(dirname "$0") [ -r VERSION ] && sed -i "s/Version [^<]*/Version $(cat VERSION)/" about-dialog.ui pyrcc5 OGAgent.qrc -o OGAgent_rc.py +[ "$(command -v pyrcc4)" ] && pyrcc4 -py3 OGAgent.qrc -o OGAgent_qt4_rc.py # process current directory ui's process +[ "$(command -v pyuic4)" ] && process_qt4 diff --git a/windows/build.bat b/windows/build.bat index 2c44474..0e0d514 100644 --- a/windows/build.bat +++ b/windows/build.bat @@ -2,5 +2,6 @@ C: CD \ogagent\src python setup.py CD .. +RENAME bin\OGAgentUser-qt4.exe OGAgentUser.exe "C:\Program Files\NSIS\makensis.exe" ogagent.nsi