source: ogAgent-Git/src/OGAgentUser.py

main 6.1.1
Last change on this file was b77b42e, checked in by Natalia Serrano <natalia.serrano@…>, 6 weeks ago

refs #2177 correctly handle UNIX signals

  • Property mode set to 100755
File size: 12.3 KB
Line 
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3#
4# Copyright (c) 2014 Virtual Cable S.L.
5# All rights reserved.
6#
7# Redistribution and use in source and binary forms, with or without modification,
8# are permitted provided that the following conditions are met:
9#
10#    * Redistributions of source code must retain the above copyright notice,
11#      this list of conditions and the following disclaimer.
12#    * Redistributions in binary form must reproduce the above copyright notice,
13#      this list of conditions and the following disclaimer in the documentation
14#      and/or other materials provided with the distribution.
15#    * Neither the name of Virtual Cable S.L. nor the names of its contributors
16#      may be used to endorse or promote products derived from this software
17#      without specific prior written permission.
18#
19# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29"""
30@author: Adolfo Gómez, dkmaster at dkmon dot com
31"""
32import atexit
33import base64
34import json
35import sys
36import time
37import os
38import socket
39import signal
40from PyQt6 import QtCore, QtGui, QtWidgets
41
42from about_dialog_ui import Ui_OGAAboutDialog
43from message_dialog_ui import Ui_OGAMessageDialog
44from opengnsys import VERSION, ipc, operations, utils
45from opengnsys.config import readConfig
46from opengnsys.loader import loadModules
47from opengnsys.log import logger
48from opengnsys.jobmgr import JobMgr
49from opengnsys.service import IPC_PORT
50
51trayIcon = None
52
53
54def sigAtExit():
55    if trayIcon:
56        trayIcon.quit()
57
58
59# About dialog
60class OGAAboutDialog(QtWidgets.QDialog):
61    def __init__(self, parent=None):
62        QtWidgets.QDialog.__init__(self, parent)
63        self.ui = Ui_OGAAboutDialog()
64        self.ui.setupUi(self)
65        self.ui.VersionLabel.setText("Version " + VERSION)
66
67    def closeDialog(self):
68        self.hide()
69
70
71class OGAMessageDialog(QtWidgets.QDialog):
72    def __init__(self, parent=None):
73        QtWidgets.QDialog.__init__(self, parent)
74        self.ui = Ui_OGAMessageDialog()
75        self.ui.setupUi(self)
76
77    def message(self, message):
78        self.ui.message.setText(message)
79        self.show()
80
81    def closeDialog(self):
82        self.hide()
83
84
85class MessagesProcessor(QtCore.QThread):
86    logoff = QtCore.pyqtSignal(name='logoff')
87    message = QtCore.pyqtSignal(tuple, name='message')
88    script = QtCore.pyqtSignal(str, name='script')
89    exit = QtCore.pyqtSignal(name='exit')
90
91    def __init__(self, port):
92        super(self.__class__, self).__init__()
93        # Retries connection for a while
94        for _ in range(10):
95            try:
96                self.ipc = ipc.ClientIPC(port)
97                self.ipc.start()
98                break
99            except Exception:
100                logger.debug('IPC Server is not reachable')
101                self.ipc = None
102                time.sleep(2)
103
104        self.running = False
105
106    def stop(self):
107        self.running = False
108        if self.ipc:
109            self.ipc.stop()
110
111    def isAlive(self):
112        return self.ipc is not None
113
114    def sendLogin(self, user_data):
115        if self.ipc:
116            self.ipc.sendLogin(user_data)
117
118    def sendLogout(self, username):
119        if self.ipc:
120            self.ipc.sendLogout(username)
121
122    def sendMessage(self, module, message, data):
123        if self.ipc:
124            self.ipc.sendMessage(module, message, data)
125
126    def run(self):
127        if self.ipc is None:
128            return
129        self.running = True
130
131        # Wait a bit so we ensure IPC thread is running...
132        time.sleep(2)
133
134        while self.running and self.ipc.running:
135            try:
136                msg = self.ipc.getMessage()
137                if msg is None:
138                    break
139                msg_id, data = msg
140                logger.debug('Got Message on User Space: {}:{}'.format(msg_id, data))
141                if msg_id == ipc.MSG_MESSAGE:
142                    module, message, data = data.decode('utf-8').split('\0')
143                    self.message.emit((module, message, data))
144                elif msg_id == ipc.MSG_LOGOFF:
145                    self.logoff.emit()
146                elif msg_id == ipc.MSG_SCRIPT:
147                    self.script.emit(data.decode('utf-8'))
148            except Exception as e:
149                logger.error('Got error on IPC thread {}'.format(utils.exceptionToMessage(e)))
150
151        if self.ipc.running is False and self.running is True:
152            logger.warn('Lost connection with Service, closing program')
153
154        self.exit.emit()
155
156
157class OGASystemTray(QtWidgets.QSystemTrayIcon):
158    jobmgr = JobMgr()
159    def __init__(self, app_, parent=None):
160        self.app = app_
161        self.config = readConfig(client=True)
162        self.modules = None
163        # Get opengnsys section as dict
164        cfg = dict(self.config.items('opengnsys'))
165        # Set up log level
166        logger.setLevel(cfg.get('log', 'INFO'))
167
168        self.ipcport = int(cfg.get('ipc_port', IPC_PORT))
169
170        QtCore.QDir.addSearchPath('images', os.path.join(os.path.dirname(__file__), 'img'))
171        icon = QtGui.QIcon('images:oga.png')
172
173        QtWidgets.QSystemTrayIcon.__init__(self, icon, parent)
174        self.menu = QtWidgets.QMenu(parent)
175        exit_action = self.menu.addAction("About")
176        exit_action.triggered.connect(self.about)
177        self.setContextMenu(self.menu)
178        self.ipc = MessagesProcessor(self.ipcport)
179
180        if self.ipc.isAlive() is False:
181            raise Exception('No connection to service, exiting.')
182
183        self.timer = QtCore.QTimer()
184        self.timer.timeout.connect(self.timerFnc)
185
186        self.stopped = False
187
188        self.ipc.message.connect(self.message)
189        self.ipc.exit.connect(self.quit)
190        self.ipc.script.connect(self.executeScript)
191        self.ipc.logoff.connect(self.logoff)
192
193        self.aboutDlg = OGAAboutDialog()
194        self.msgDlg = OGAMessageDialog()
195
196        self.timer.start(1000)  # Launch idle checking every 1 seconds
197
198        self.ipc.start()
199
200    def initialize(self):
201        # Load modules and activate them
202        # Also, sends "login" event to service
203        self.modules = loadModules(self, client=True)
204        logger.debug('Modules: {}'.format(list(v.name for v in self.modules)))
205
206        # Send init to all modules
207        valid_mods = []
208        for mod in self.modules:
209            try:
210                logger.debug('Activating module {}'.format(mod.name))
211                mod.activate()
212                valid_mods.append(mod)
213            except Exception as e:
214                logger.exception()
215                logger.debug ("Activation of {} failed: {}".format(mod.name, utils.exceptionToMessage(e)))
216        self.modules[:] = valid_mods  # copy instead of assignment
217        # If this is running, it's because he have logged in, inform service of this fact
218        self.ipc.sendLogin((operations.getCurrentUser(), operations.getSessionLanguage(),
219                            operations.get_session_type()))
220
221    def deinitialize(self):
222        for mod in reversed(self.modules):  # Deinitialize reversed of initialization
223            try:
224                logger.debug('Deactivating module {}'.format(mod.name))
225                mod.deactivate()
226            except Exception as e:
227                logger.exception()
228                logger.debug ("Deactivation of {} failed: {}".format(mod.name, utils.exceptionToMessage(e)))
229
230    def timerFnc(self):
231        pass
232
233    def message(self, msg):
234        """
235        Processes the message sent asynchronously, msg is an QString
236        """
237        try:
238            logger.debug('msg: {}, {}'.format(type(msg), msg))
239            module, message, data = msg
240        except Exception as e:
241            logger.debug ('Got exception {} processing message {}'.format(e, msg))
242            return
243
244        for v in self.modules:
245            if v.name == module:  # Case Sensitive!!!!
246                try:
247                    logger.debug('Notifying message {} to module {} with json data {}'.format(message, v.name, data))
248                    v.processMessage(message, json.loads(data))
249                    return
250                except Exception as e:
251                    logger.debug ('Got exception {} processing generic message on {}'.format(e, v.name))
252
253        logger.debug ('Module {} not found, messsage {} not sent'.format(module, message))
254
255    ## when is this run??
256    def executeScript(self, script):
257        script = base64.b64decode(script.encode('ascii'))
258        logger.debug('Executing received script "{}"'.format(script))
259        self.jobmgr.launch_job (script, True)
260
261    def logoff(self):
262        logger.debug('Logoff invoked')
263        operations.logoff()  # Invoke log off
264
265    def about(self):
266        self.aboutDlg.exec()
267
268    def cleanup(self):
269        logger.debug('Quit invoked')
270        if self.stopped is False:
271            self.stopped = True
272            try:
273                self.deinitialize()
274            except Exception:
275                logger.exception()
276                logger.debug ('Got exception deinitializing modules')
277
278            try:
279                # If we close Client, send Logoff to Broker
280                self.ipc.sendLogout(operations.getCurrentUser())
281                time.sleep(1)
282                self.timer.stop()
283                self.ipc.stop()
284            except Exception:
285                # May we have lost connection with server, simply log and exit in that case
286                logger.exception()
287                logger.debug ('Got an exception, processing quit')
288
289            try:
290                # operations.logoff()  # Uncomment this after testing to logoff user
291                pass
292            except Exception:
293                pass
294
295    def quit(self):
296        # logger.debug("Exec quit {}".format(self.stopped))
297        if self.stopped is False:
298            self.cleanup()
299            self.app.quit()
300
301    def closeEvent(self, event):
302        logger.debug("Exec closeEvent")
303        event.accept()
304        self.quit()
305
306
307if __name__ == '__main__':
308    app = QtWidgets.QApplication(sys.argv)
309
310    if not QtWidgets.QSystemTrayIcon.isSystemTrayAvailable():
311        # QtGui.QMessageBox.critical(None, "Systray", "I couldn't detect any system tray on this system.")
312        sys.exit(1)
313
314    # This is important so our app won't close on messages windows (alerts, etc...)
315    QtWidgets.QApplication.setQuitOnLastWindowClosed(False)
316
317    try:
318        trayIcon = OGASystemTray(app)
319    except Exception as e:
320        logger.exception()
321        logger.debug ('OGA Service is not running, or it can\'t contact with OGA Server. User Tools stopped: {}'.format(
322            utils.exceptionToMessage(e)))
323        sys.exit(1)
324
325    try:
326        trayIcon.initialize()  # Initialize modules, etc..
327    except Exception as e:
328        logger.exception()
329        logger.debug ('Exception initializing OpenGnsys User Agent {}'.format(utils.exceptionToMessage(e)))
330        trayIcon.quit()
331        sys.exit(1)
332
333    ## begin SIGTERM handling
334    signal_socket = socket.socketpair()
335    signal_socket[0].setblocking(False)
336    signal_socket[1].setblocking(False)
337    signal.set_wakeup_fd(signal_socket[0].fileno())
338
339    def signal_handler(signum, frame):
340        #print (f"Received signal {signum}")
341        pass
342
343    def qt_signal_handler():
344        data = signal_socket[1].recv(1)
345        #print(f"Signal ({data}) received via socket, shutting down gracefully...")
346        if trayIcon:
347            trayIcon.quit()
348
349    signal.signal(signal.SIGTERM, signal_handler)
350    signal.signal(signal.SIGINT, signal_handler)
351
352    notifier = QtCore.QSocketNotifier(signal_socket[1].fileno(), QtCore.QSocketNotifier.Type.Read)
353    notifier.activated.connect(qt_signal_handler)
354    ## end SIGTERM handling
355
356    app.aboutToQuit.connect(trayIcon.cleanup)
357    trayIcon.show()
358
359    # Catch kill and logout user :)
360    atexit.register(sigAtExit)
361
362    res = app.exec()
363
364    logger.debug('Exiting')
365    trayIcon.quit()
366
367    sys.exit(res)
Note: See TracBrowser for help on using the repository browser.