ogagent/src/OGAgentUser.py

368 lines
12 KiB
Python

#!/usr/bin/env python3
# -*- 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
"""
import atexit
import base64
import json
import sys
import time
import os
import socket
import signal
from PyQt6 import QtCore, QtGui, QtWidgets
from about_dialog_ui import Ui_OGAAboutDialog
from message_dialog_ui import Ui_OGAMessageDialog
from opengnsys import VERSION, ipc, operations, utils
from opengnsys.config import readConfig
from opengnsys.loader import loadModules
from opengnsys.log import logger
from opengnsys.jobmgr import JobMgr
from opengnsys.service import IPC_PORT
trayIcon = None
def sigAtExit():
if trayIcon:
trayIcon.quit()
# About dialog
class OGAAboutDialog(QtWidgets.QDialog):
def __init__(self, parent=None):
QtWidgets.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(QtWidgets.QDialog):
def __init__(self, parent=None):
QtWidgets.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(str, 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 sendMessage(self, module, message, data):
if self.ipc:
self.ipc.sendMessage(module, message, data)
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.decode('utf-8').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(data.decode('utf-8'))
except Exception as e:
logger.error('Got error on IPC thread {}'.format(utils.exceptionToMessage(e)))
if self.ipc.running is False and self.running is True:
logger.warn('Lost connection with Service, closing program')
self.exit.emit()
class OGASystemTray(QtWidgets.QSystemTrayIcon):
jobmgr = JobMgr()
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))
QtCore.QDir.addSearchPath('images', os.path.join(os.path.dirname(__file__), 'img'))
icon = QtGui.QIcon('images:oga.png')
QtWidgets.QSystemTrayIcon.__init__(self, icon, parent)
self.menu = QtWidgets.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.debug ("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.debug ("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.debug ('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.debug ('Got exception {} processing generic message on {}'.format(e, v.name))
logger.debug ('Module {} not found, messsage {} not sent'.format(module, message))
## when is this run??
def executeScript(self, script):
script = base64.b64decode(script.encode('ascii'))
logger.debug('Executing received script "{}"'.format(script))
self.jobmgr.launch_job (script, True)
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.debug ('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.debug ('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 = QtWidgets.QApplication(sys.argv)
if not QtWidgets.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...)
QtWidgets.QApplication.setQuitOnLastWindowClosed(False)
try:
trayIcon = OGASystemTray(app)
except Exception as e:
logger.exception()
logger.debug ('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.debug ('Exception initializing OpenGnsys User Agent {}'.format(utils.exceptionToMessage(e)))
trayIcon.quit()
sys.exit(1)
## begin SIGTERM handling
signal_socket = socket.socketpair()
signal_socket[0].setblocking(False)
signal_socket[1].setblocking(False)
signal.set_wakeup_fd(signal_socket[0].fileno())
def signal_handler(signum, frame):
#print (f"Received signal {signum}")
pass
def qt_signal_handler():
data = signal_socket[1].recv(1)
#print(f"Signal ({data}) received via socket, shutting down gracefully...")
if trayIcon:
trayIcon.quit()
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
notifier = QtCore.QSocketNotifier(signal_socket[1].fileno(), QtCore.QSocketNotifier.Type.Read)
notifier.activated.connect(qt_signal_handler)
## end SIGTERM handling
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)