#!/usr/bin/env python3 """ Main entry point for the application. """ import sys import os import os.path import threading import time import traceback import logging import urllib.request from urllib.error import URLError import PyQt5.QtWebEngineWidgets from PyQt5.QtWidgets import QApplication, QMessageBox from PyQt5.QtCore import Qt, QSettings, QDir import PyQt5.QtCore import click from vasl_templates.webapp.utils import SimpleError # FUDGE! This needs to be created before showing any UI elements e.g. an error message box. qt_app = QApplication( sys.argv ) app_settings = None _webapp_error = None # nb: this needs to be global :shrug: # --------------------------------------------------------------------- _QT_LOGGING_LEVELS = { PyQt5.QtCore.QtCriticalMsg: logging.CRITICAL, PyQt5.QtCore.QtFatalMsg: logging.ERROR, PyQt5.QtCore.QtWarningMsg: logging.WARNING, PyQt5.QtCore.QtInfoMsg: logging.INFO, PyQt5.QtCore.QtDebugMsg: logging.DEBUG, } def qtMessageHandler( msg_type, context, msg ):# pylint: disable=unused-argument """Handle PyQt logging messages.""" # FUDGE! PyQt issues a bunch of warning messages because we had to proxy WebChannel requests # via the MainWindow object - we filter them out here. if "has no notify signal and is not constant" in msg: return logging.getLogger( "qt" ).log( _QT_LOGGING_LEVELS[msg_type], "%s", msg ) # --------------------------------------------------------------------- @click.command() @click.option( "--template-pack", help="Template pack to auto-load (ZIP file or directory)." ) @click.option( "--default-scenario", help="Default scenario settings." ) @click.option( "--remote-debugging", help="Chrome DevTools port number." ) @click.option( "--debug", help="Debug config file." ) def main( template_pack, default_scenario, remote_debugging, debug ): """Manage HTML labels in a VASL scenario.""" try: return _do_main( template_pack, default_scenario, remote_debugging, debug ) except Exception as ex: #pylint: disable=broad-except # log the error # NOTE: If we get here, there was probably an error during startup, so we can't # assume too much about how much of our expected environment has been set up. try: fname = os.path.join( QDir.homePath(), "vasl-templates.log" ) with open( fname, "w" ) as fp: traceback.print_exc( file=fp ) except: #pylint: disable=bare-except pass QMessageBox.warning( None, "Unexpected error", str(ex) ) return -1 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - def _do_main( template_pack, default_scenario, remote_debugging, debug ): #pylint: disable=too-many-locals,too-many-branches """Do main processing.""" # NOTE: We do these imports here (instead of at the top of the file) so that we can catch errors. from vasl_templates.webapp import app as webapp from vasl_templates.webapp import load_debug_config from vasl_templates.webapp import main as webapp_main, snippets as webapp_snippets # configure the default template pack if template_pack: if template_pack.lower().endswith( ".zip" ): rc = os.path.isfile( template_pack ) else: rc = os.path.isdir( template_pack ) if not rc: click.echo( "ERROR: The template pack must be a ZIP file, or a directory containing the template files." ) return 1 webapp_snippets.default_template_pack = template_pack # configure the default scenario if default_scenario: if not os.path.isfile( default_scenario ): raise SimpleError( "Can't find the default scenario file." ) webapp_main.default_scenario = default_scenario # configure remote debugging if remote_debugging: remote_debugging = remote_debugging.replace( "localhost", "127.0.0.1" ) os.environ["QTWEBENGINE_REMOTE_DEBUGGING"] = remote_debugging # load the application settings app_settings_fname = "vasl-templates.ini" if sys.platform == "win32" else ".vasl-templates.conf" if not os.path.isfile( app_settings_fname ) : app_settings_fname = os.path.join( QDir.homePath(), app_settings_fname ) # FUDGE! Declaring app_settings as global here doesn't work on Windows (?!), we have to do this weird import :-/ import vasl_templates.main #pylint: disable=import-self vasl_templates.main.app_settings = QSettings( app_settings_fname, QSettings.IniFormat ) # install the debug config file if debug: load_debug_config( debug ) # connect PyQt's logging to Python logging PyQt5.QtCore.qInstallMessageHandler( qtMessageHandler ) # install the server settings try: from vasl_templates.server_settings import install_server_settings #pylint: disable=cyclic-import install_server_settings( True ) except Exception as ex: #pylint: disable=broad-except # NOTE: We used to advise the user to check the app config file for errors, but exceptions can be thrown # for reasons other than errors in that file (e.g. bad JSON in the vehicle/ordnance data files). logging.critical( traceback.format_exc() ) from vasl_templates.main_window import MainWindow #pylint: disable=cyclic-import MainWindow.showErrorMsg( "Couldn't install the server settings:\n\n{}".format( ex ) ) return 2 # disable the Flask "do not use in a production environment" warning import flask.cli flask.cli.show_server_banner = lambda *args: None # start the webapp server port = webapp.config[ "FLASK_PORT_NO" ] def webapp_thread(): """Run the webapp server.""" try: webapp.run( host="localhost", port=port, use_reloader=False ) except Exception as ex: #pylint: disable=broad-except logging.critical( "WEBAPP SERVER EXCEPTION: %s", ex ) logging.critical( traceback.format_exc() ) # NOTE: We pass the exception to the GUI thread, where it can be shown to the user. global _webapp_error _webapp_error = ex thread = threading.Thread( target=webapp_thread ) # FUDGE! If we detect another instance, we hang on Windows after reporting the error. Running the webapp # in a daemon thread makes the problem go away - you would think the thread would terminate, since it wouldn't # be able to listen on the same server port - but I guess not :-/ thread.daemon = True thread.start() # NOTE: We want to detect if another instance of the program is already running, but we can't simply # try to connect to the webapp, since we can't tell the difference between connecting to the webapp # we just started above, and an already-running instance. We handle this by assigning each instance # a unique ID, which lets us figure out if we've connected to ourself, or another instance. from vasl_templates.webapp.main import INSTANCE_ID # wait for the webapp server to start while True: if _webapp_error: break try: url = "http://localhost:{}/ping".format( port ) resp = urllib.request.urlopen( url ).read().decode( "utf-8" ) # we got a response - figure out if we connected to ourself or another instance if resp[:6] != "pong: ": raise SimpleError( "Unexpected server check response: {}".format( resp ) ) if resp[6:] == INSTANCE_ID: break else: from vasl_templates.webapp.config.constants import APP_NAME QMessageBox.warning( None, APP_NAME, "The program is already running." ) return -1 except URLError: # no response - the webapp server is probably still starting up time.sleep( 0.25 ) continue except Exception as ex: #pylint: disable=broad-except raise ex if _webapp_error: # the webapp server didn't start up - re-raise the error in this thread raise _webapp_error #pylint: disable=raising-bad-type # check if we should disable OpenGL # Using the QWebEngineView crashes on Windows 7 in a VM. It uses OpenGL, which is # apparently not well supported on Windows, and is dependent on the graphics card driver: # https://stackoverflow.com/a/50393872 # https://stackoverflow.com/questions/33090346/is-there-any-way-to-use-qtwebengine-without-opengl # Switching to software rendering (AA_UseSoftwareOpenGL) got things going :shrug: # Also see: https://doc.qt.io/qt-5/windows-requirements.html opengl_type = webapp.config.get( "OPENGL_TYPE" ) if opengl_type: logging.info( "Setting OpenGL: %s", opengl_type ) opengl_type = getattr( Qt, opengl_type ) QApplication.setAttribute( opengl_type ) # check if we should disable the embedded browser disable_browser = webapp.config.get( "DISABLE_WEBENGINEVIEW" ) # run the application url = "http://localhost:{}".format( port ) from vasl_templates.main_window import MainWindow #pylint: disable=cyclic-import main_window = MainWindow( url, disable_browser ) main_window.show() ret_code = qt_app.exec_() return ret_code # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if __name__ == "__main__": sys.exit( main() ) #pylint: disable=no-value-for-parameter