2b4d6f6d93520b545b8c74925112983231a81632
   1import sys
   2sys.coinit_flags= 0
   3import win32com.client
   4import pywintypes
   5import os
   6import shutil
   7import socketserver
   8import threading
   9import asyncio
  10import websockets
  11import logging, logging.handlers
  12import json
  13import urllib
  14import posixpath
  15import time
  16import pythoncom
  17import pystray
  18from PIL import Image
  19from copy import copy
  20
  21import ppt_control.__init__ as pkg_base
  22import ppt_control.http_server_39 as http_server    # 3.9 version of the HTTP server (details in module)
  23import ppt_control.config as config
  24
  25logging.basicConfig()
  26
  27global http_daemon
  28http_daemon = None
  29global http_server
  30http_server = None
  31global ws_daemon
  32ws_daemon = None
  33global users
  34users = set()
  35global logger
  36logger = None
  37global refresh_daemon
  38refresh_daemon = None
  39global icon
  40icon = None
  41global ppt_application
  42ppt_application = None
  43global ppt_presentations
  44ppt_presentations = {}
  45global disable_protected_attempted
  46disable_protected_attempted = set()
  47
  48PKG_NAME = pkg_base.__name__
  49PKG_VERSION = pkg_base.__version__
  50CONFIG_FILE = r'''..\ppt-control.ini'''
  51LOGFILE = r'''..\ppt-control.log'''
  52
  53
  54class Handler(http_server.SimpleHTTPRequestHandler):
  55    """
  56    Custom handler to translate /cache* urls to the cache directory (set in the config)
  57    """
  58    def __init__(self, *args, **kwargs):
  59        super().__init__(*args, directory=os.path.dirname(os.path.realpath(__file__)) + r'''\static''')
  60        
  61    def log_request(self, code='-', size='-'):
  62        return
  63
  64        
  65    def translate_path(self, path):
  66        """Translate a /-separated PATH to the local filename syntax.
  67
  68        Components that mean special things to the local file system
  69        (e.g. drive or directory names) are ignored.  (XXX They should
  70        probably be diagnosed.)
  71
  72        """
  73        # abandon query parameters
  74        path = path.split('?',1)[0]
  75        path = path.split('#',1)[0]
  76        # Don't forget explicit trailing slash when normalizing. Issue17324
  77        trailing_slash = path.rstrip().endswith('/')
  78        try:
  79            path = urllib.parse.unquote(path, errors='surrogatepass')
  80        except UnicodeDecodeError:
  81            path = urllib.parse.unquote(path)
  82        path = posixpath.normpath(path)
  83        words = path.split('/')
  84        words = list(filter(None, words))
  85        if len(words) > 0 and words[0] == "cache":
  86            if words[1] in ppt_presentations:
  87                path = config.prefs["Main"]["cache"]
  88            else:
  89                path = "black.jpg"
  90                logger.warning("Request for cached file {} for non-existent presentation".format(path))
  91            words.pop(0)
  92        else:
  93            path = self.directory
  94        for word in words:
  95            if os.path.dirname(word) or word in (os.curdir, os.pardir):
  96                # Ignore components that are not a simple file/directory name
  97                continue
  98            path = os.path.join(path, word)
  99        if trailing_slash:
 100            path += '/'
 101        return path
 102
 103async def ws_handler(websocket, path):
 104    """
 105    Handle a WebSockets connection
 106    """
 107    logger.debug("Handling WebSocket connection")
 108    recv_task = asyncio.ensure_future(ws_receive(websocket, path))
 109    send_task = asyncio.ensure_future(ws_send(websocket, path))
 110    done, pending = await asyncio.wait(
 111        [recv_task, send_task],
 112        return_when=asyncio.FIRST_COMPLETED,
 113    )
 114    for task in pending:
 115        task.cancel()
 116
 117async def ws_receive(websocket, path):
 118    """
 119    Process data received on the WebSockets connection
 120    """
 121    users.add(websocket)
 122    try:
 123        # Send initial state to clients on load
 124        for pres in ppt_presentations:
 125            broadcast_presentation(ppt_presentations[pres])
 126        async for message in websocket:
 127            logger.debug("Received websocket message: " + str(message))
 128            data = json.loads(message)
 129            pres = ppt_presentations[data["presentation"]]
 130            if data["action"] == "prev":
 131                pres.prev()
 132            elif data["action"] == "next":
 133                pres.next()
 134            elif data["action"] == "first":
 135                pres.first()
 136            elif data["action"] == "last":
 137                pres.last()
 138            elif data["action"] == "black":
 139                if pres.state() == 3:
 140                    pres.normal()
 141                else:
 142                    pres.black()
 143            elif data["action"] == "white":
 144                if pres.state() == 4:
 145                    pres.normal()
 146                else:
 147                    pres.white()
 148            elif data["action"] == "goto":
 149                pres.goto(int(data["value"]))
 150            else:
 151                logger.error("Received unnsupported event: {}", data)
 152    finally:
 153        users.remove(websocket)
 154
 155async def ws_send(websocket, path):
 156    """
 157    Broadcast data to all WebSockets clients
 158    """
 159    while True:
 160        message = await ws_queue.get()
 161        await asyncio.wait([user.send(message) for user in users])
 162
 163
 164def run_http():
 165    """
 166    Start the HTTP server
 167    """
 168    global http_server
 169    http_server = http_server.HTTPServer((config.prefs["HTTP"]["interface"], config.prefs.getint("HTTP", "port")), Handler)
 170    http_server.serve_forever()
 171
 172
 173def run_ws():
 174    """
 175    Set up threading/async for WebSockets server
 176    """
 177    # https://stackoverflow.com/questions/21141217/how-to-launch-win32-applications-in-separate-threads-in-python/22619084#22619084
 178    # https://www.reddit.com/r/learnpython/comments/mwt4qi/pywintypescom_error_2147417842_the_application/
 179    pythoncom.CoInitializeEx(pythoncom.COINIT_MULTITHREADED)
 180    asyncio.set_event_loop(asyncio.new_event_loop())
 181    global ws_queue
 182    ws_queue = asyncio.Queue()
 183    global loop
 184    loop = asyncio.get_event_loop()
 185    start_server = websockets.serve(ws_handler, config.prefs["WebSocket"]["interface"], config.prefs.getint("WebSocket", "port"), ping_interval=None)
 186    asyncio.get_event_loop().run_until_complete(start_server)
 187    asyncio.get_event_loop().run_forever()
 188
 189def setup_http():
 190    """
 191    Set up threading for HTTP server
 192    """
 193    http_daemon = threading.Thread(name="http_daemon", target=run_http)
 194    http_daemon.setDaemon(True)
 195    http_daemon.start()
 196    logger.info("Started HTTP server")
 197
 198def setup_ws():
 199    """
 200    Set up threading for WebSockets server
 201    """
 202    global ws_daemon
 203    ws_daemon = threading.Thread(name="ws_daemon", target=run_ws)
 204    ws_daemon.setDaemon(True)
 205    ws_daemon.start()
 206    logger.info("Started websocket server")
 207    
 208
 209def broadcast_presentation(pres):
 210    """
 211    Broadcast the state of a single presentation to all connected clients. Also ensures the current
 212    slide and the two upcoming slides are exported and cached.
 213    """
 214    name = pres.presentation.Name
 215    pres_open = name in ppt_presentations
 216    slideshow = pres.slideshow is not None
 217    visible = pres.state()
 218    slide_current = pres.slide_current()
 219    slide_total = pres.slide_total()
 220
 221    pres.export_current_next()
 222    
 223    if users:   # asyncio.wait doesn't accept an empty list
 224        state = {"name": name, "pres_open": pres_open, "slideshow": slideshow, "visible": visible,
 225                    "slide_current": slide_current, "slide_total": slide_total}
 226        loop.call_soon_threadsafe(ws_queue.put_nowait, json.dumps({"type": "state", **state}))
 227
 228class ApplicationEvents:
 229    """
 230    Events assigned to the root application.
 231    Ref: https://docs.microsoft.com/en-us/office/vba/api/powerpoint.application#events
 232    """
 233    def OnSlideShowNextSlide(self, window, *args):
 234        """
 235        Triggered when the current slide number of any slideshow is incremented, locally or through
 236        ppt_control.
 237        """
 238        logger.debug("Slide advanced for {}".format(window.Presentation.Name))
 239        broadcast_presentation(ppt_presentations[window.Presentation.Name])
 240
 241    def OnSlideShowPrevSlide(self, window, *args):
 242        """
 243        Triggered when the current slide number of any slideshow is decremented, locally or through
 244        ppt_control.
 245        """
 246        logger.debug("Slide decremented for {}".format(window.Presentation.Name))
 247        broadcast_presentation(ppt_presentations[window.Presentation.Name])
 248    
 249    def OnAfterPresentationOpen(self, presentation, *args):
 250        """
 251        Triggered when an existing presentation is opened. This adds the newly opened presentation
 252        to the list of open presentations.
 253        """
 254        logger.debug("Presentation {} opened - adding to list".format(presentation.Name))
 255        global ppt_presentations
 256        ppt_presentations[presentation.Name] = Presentation(ppt_application, pres_obj=presentation)
 257        broadcast_presentation(ppt_presentations[presentation.Name])
 258        disable_protected_attempted.discard(presentation.Name)
 259        icon.notify("Connected to {}".format(presentation.Name), PKG_NAME)
 260
 261    def OnAfterNewPresentation(self, presentation, *args):
 262        """
 263        Triggered when a new presentation is opened. This adds the new presentation to the list
 264        of open presentations.
 265        """
 266        logger.debug("Presentation {} opened - adding to list".format(presentation.Name))
 267        global ppt_presentations
 268        ppt_presentations[presentation.Name] = Presentation(ppt_application, pres_obj=presentation)
 269        broadcast_presentation(ppt_presentations[presentation.Name])
 270        icon.notify("Connected to {}".format(presentation.Name), PKG_NAME)
 271
 272    def OnPresentationClose(self, presentation, *args):
 273        """
 274        Triggered when a presentation is closed. This removes the presentation from the list of
 275        open presentations. A delay of 200 ms is included to make sure the presentation is 
 276        actually closed, since the event is called simultaneously as the presentation is removed
 277        from PowerPoint's internal structure. Ref:
 278        https://docs.microsoft.com/en-us/office/vba/api/powerpoint.application.presentationclose
 279        """
 280        logger.debug("Presentation {} closed - removing from list".format(presentation.Name))
 281        global ppt_presentations
 282        time.sleep(0.2)
 283        broadcast_presentation(ppt_presentations.pop(presentation.Name))
 284        icon.notify("Disconnected from {}".format(presentation.Name), PKG_NAME)
 285
 286    def OnSlideShowBegin(self, window, *args):
 287        """
 288        Triggered when a slideshow is started. This initialises the Slideshow object in the 
 289        appropriate Presentation object.
 290        """
 291        logger.debug("Slideshow started for {}".format(window.Presentation.Name))
 292        global ppt_presentations
 293        ppt_presentations[window.Presentation.Name].slideshow = window
 294        broadcast_presentation(ppt_presentations[window.Presentation.Name])
 295    
 296    def OnSlideShowEnd(self, presentation, *args):
 297        """
 298        Triggered when a slideshow is ended. This deinitialises the Slideshow object in the 
 299        appropriate Presentation object.
 300        """
 301        logger.debug("Slideshow ended for {}".format(presentation.Name))
 302        global ppt_presentations
 303        ppt_presentations[presentation.Name].slideshow = None
 304        broadcast_presentation(ppt_presentations[presentation.Name])
 305
 306
 307class Presentation:
 308    """
 309    Class representing an instance of PowerPoint with a file open (so-called "presentation"
 310    in PowerPoint terms). Mostly just a wrapper for PowerPoint's `Presentation` object.
 311    """
 312    def __init__(self, application, pres_index=None, pres_obj=None):
 313        """
 314        Initialise a Presentation object.
 315            application     The PowerPoint application which the presentation is being run within
 316            pres_index      PowerPoint's internal presentation index (NOTE this is indexed from 1)
 317        """
 318        if pres_index == None and pres_obj == None:
 319            raise ValueError("Cannot initialise a presentation without a presentation ID or object")
 320        assert len(application.Presentations) > 0, "Cannot initialise presentation from application with no presentations"
 321
 322        self.__application = application
 323        if pres_obj is not None:
 324            self.presentation = pres_obj
 325        else:
 326            self.presentation = application.Presentations(pres_index)
 327        self.slideshow = self.get_slideshow()
 328
 329
 330    def get_slideshow(self):
 331        """
 332        Check whether the presentation is in slideshow mode, and if so, return the SlideShowWindow.
 333        """
 334        try:
 335            return self.presentation.SlideShowWindow
 336        except pywintypes.com_error as exc:
 337            logger.debug("Couldn't get slideshow for {}: {}".format(self.presentation.Name, exc))
 338            return None
 339
 340    def state(self):
 341        """
 342        Returns the visibility state of the slideshow:
 343        2: normal
 344        3: black
 345        4: white
 346        5: done
 347        """
 348        if self.slideshow is not None:
 349            return self.slideshow.View.State
 350        else:
 351            return 2
 352
 353    def slide_current(self):
 354        """
 355        Returns the current slide number of the slideshow, or 0 if no slideshow is running.
 356        """
 357        if self.slideshow is not None:
 358            return self.slideshow.View.CurrentShowPosition
 359        else:
 360            return 0
 361
 362    def slide_total(self):
 363        """
 364        Returns the total number of slides in the presentation, regardless of whether a slideshow
 365        is running.
 366        """
 367        return self.presentation.Slides.Count
 368
 369    def prev(self):
 370        """
 371        Go to the previous slide if there is a slideshow running. Notifying clients of the new state
 372        is managed by the ApplicationEvent.
 373        """
 374        assert self.slideshow is not None, "Slideshow is not running for {}".format(self.presentation.Name)
 375        self.slideshow.View.Previous()
 376
 377    def next(self):
 378        """
 379        Go to the previous slide if there is a slideshow running. Notifying clients of the new state
 380        is managed by the ApplicationEvent.
 381        """
 382        assert self.slideshow is not None, "Slideshow is not running for {}".format(self.presentation.Name)
 383        self.slideshow.View.Next()
 384
 385    def first(self):
 386        """
 387        Go to the first slide if there is a slideshow running. Notifying clients of the new state
 388        is managed by the ApplicationEvent.
 389        """
 390        assert self.slideshow is not None, "Slideshow is not running for {}".format(self.presentation.Name)
 391        self.slideshow.View.First()
 392                
 393    def last(self):
 394        """
 395        Go to the last slide if there is a slideshow running. Notifying clients of the new state
 396        is managed by the ApplicationEvent.
 397        """
 398        assert self.slideshow is not None, "Slideshow is not running for {}".format(self.presentation.Name)
 399        self.slideshow.View.Last()
 400
 401    def goto(self, slide):
 402        """
 403        Go to a numbered slide if there is a slideshow running. Notifying clients of the new state
 404        is managed by the ApplicationEvent.
 405        """
 406        assert self.slideshow is not None, "Slideshow is not running for {}".format(self.presentation.Name)
 407        if slide <= self.slide_total():
 408            self.slideshow.View.GotoSlide(slide)
 409        else:
 410            self.last()
 411            self.next()
 412
 413    def normal(self):
 414        """
 415        Make the slideshow visible if there is a slideshow running.
 416        """
 417        assert self.slideshow is not None, "Slideshow is not running for {}".format(self.presentation.Name)
 418        self.slideshow.View.State = 2
 419        broadcast_presentation(self)
 420
 421    def black(self):
 422        """
 423        Make the slideshow black if there is a slideshow running. 
 424        """
 425        assert self.slideshow is not None, "Slideshow is not running for {}".format(self.presentation.Name)
 426        self.slideshow.View.State = 3
 427        broadcast_presentation(self)
 428
 429    def white(self):
 430        """
 431        Make the slideshow white if there is a slideshow running. 
 432        """
 433        assert self.slideshow is not None, "Slideshow is not running for {}".format(self.presentation.Name)
 434        self.slideshow.View.State = 4
 435        broadcast_presentation(self)
 436
 437    def export_current_next(self):
 438        """
 439        Export the current slide, the next slide, and the one after (ensures enough images are 
 440        always cached)
 441        """
 442        self.export(self.slide_current())
 443        self.export(self.slide_current() + 1)
 444        self.export(self.slide_current() + 2)
 445
 446    def export(self, slide):
 447        """
 448        Export a relatively low-resolution image of a slide using PowerPoint's built-in export 
 449        function. The cache destination is set in the config.
 450        """
 451        destination = config.prefs["Main"]["cache"] + "\\" + self.presentation.Name + "\\" + str(slide) + "." + config.prefs["Main"]["cache_format"].lower()
 452        logger.debug("Exporting slide {} of {}".format(slide, self.presentation.Name))
 453        os.makedirs(os.path.dirname(destination), exist_ok=True)
 454        if not os.path.exists(destination) or time.time() - os.path.getmtime(destination) > config.prefs.getint("Main", "cache_timeout"):
 455            if slide <= self.slide_total():
 456                attempts = 0
 457                while attempts < 3:
 458                    try:
 459                        self.presentation.Slides(slide).Export(destination, config.prefs["Main"]["cache_format"])
 460                        break
 461                    except:
 462                        pass
 463                    attempts += 1
 464            elif slide == self.slide_total() + 1:
 465                try:
 466                    shutil.copyfileobj(open(os.path.dirname(os.path.realpath(__file__)) + r'''\static\black.jpg''', 'rb'), open(destination, 'wb'))
 467                except Exception as exc:
 468                    logger.warning("Failed to copy black slide (number {} for presentation {}): {}".format(slide, self.presentation.Name, exc))
 469            else:
 470                pass
 471
 472    def export_all(self):
 473        """
 474        Export all slides in the presentation
 475        """
 476        for i in range(1, self.slide_total() + 2):
 477            self.export(i)
 478
 479def null_action(*args):
 480    """
 481    Placeholder for disabled menu items in systray
 482    """
 483    pass
 484                
 485def refresh():
 486    """
 487    Clear COM events and update interface elements at an interval defined in "refresh" in the config
 488    """
 489    while getattr(refresh_daemon, "do_run", True):
 490        try:
 491            pythoncom.PumpWaitingMessages()
 492            icon.menu = (pystray.MenuItem("Status: " + "dis"*(len(ppt_presentations) == 0) + "connected",
 493                lambda: null_action(), enabled=False),
 494                pystray.MenuItem("Stop", lambda: exit(icon)))
 495            manage_protected_view(ppt_application)
 496            time.sleep(float(config.prefs["Main"]["refresh"]))
 497        except Exception as exc:
 498            # Deal with any exceptions, such as RPC server restarting, by reconnecting to application
 499            # (if this fails again, that's okay because we'll keep trying until it works)
 500            logger.error("Error whilst refreshing state: {}".format(exc))
 501            app = get_application()
 502
 503        
 504
 505def get_application():
 506    """
 507    Create an Application object representing the PowerPoint application installed on the machine.
 508    This should succeed regardless of whether PowerPoint is running, as long as PowerPoint is
 509    installed.
 510    Returns the Application object if successful, otherwise returns None.
 511    """
 512    try:
 513        return win32com.client.Dispatch('PowerPoint.Application')
 514    except pywintypes.com_error:
 515        # PowerPoint is probably not installed, or other COM failure
 516        return None
 517        
 518
 519def manage_protected_view(app):
 520    """
 521    Attempt to unlock any presentations that have been opened in protected view. These cannot be
 522    controlled by the program whilst they are in protected view, so we attempt to disable protected
 523    view, or show a notification if this doesn't work for some reason.
 524    """
 525    try:
 526        if app.ProtectedViewWindows.Count > 0:
 527            logger.debug("Found open presentation(s) but at least one is in protected view")
 528            for i in range(1, app.ProtectedViewWindows.Count + 1):  # +1 to account for indexing from 1
 529                pres_name = app.ProtectedViewWindows(i).Presentation.Name
 530                if pres_name in disable_protected_attempted:
 531                    continue
 532                if config.prefs.getboolean("Main", "disable_protected"):
 533                    try:
 534                        app.ProtectedViewWindows(i).Edit()
 535                        logger.info("Enabled editing for {}".format(pres_name))
 536                    except Exception as exc:
 537                        icon.notify("Failed to disable protected view on \"{}\"".format((pres_name[:22] + '...') 
 538                        if len(pres_name) > 25 else pres_name), "Disable protected view in PowerPoint")
 539                        logger.warning("Failed to disable protected view {} for editing - do this manually: {}".format(pres_name, exc))
 540                        disable_protected_attempted.add(pres_name)
 541                else:
 542                    icon.notify("Cannot control \"{}\" in protected view".format((pres_name[:22] + '...') 
 543                        if len(pres_name) > 25 else pres_name), "Disable protected view in PowerPoint")
 544                    logger.warning("Cannot control {} in protected view, and automatic disabling of protected view is turned off".format(pres_name))
 545                    disable_protected_attempted.add(pres_name)
 546    except Exception as exc:
 547        # Sometimes gets pywintypes.com_error "The object invoked has disconnected from its clients"
 548        # at this point which leads to "Exception whilst dealing with protected view windows".
 549        logger.warning("Exception whilst dealing with protected view windows: {}".format(exc))
 550        app = get_application()
 551
 552
 553
 554def connect_ppt():
 555    """
 556    Connect to the PowerPoint COM interface and perform initial enumeration of open files 
 557    ("presentations"). Files that are subsequently opened are dealt with using COM events (see the
 558    ApplicationEvents class above). Therefore, once we are finished setting things up, we just 
 559    call refresh() as a daemon in order to keep clients up to date.
 560    """
 561    logger.debug("Searching for a PowerPoint slideshow...")
 562    global ppt_application
 563    global ppt_presentations
 564
 565    # Initialise PowerPoint application
 566    ppt_application = get_application()
 567    if ppt_application is None:
 568        # Couldn't find PowerPoint application
 569        icon.notify("Couldn't find PowerPoint application", "Error starting {}".format(PKG_NAME))
 570        logger.error("Couldn't find PowerPoint application - check that PowerPoint is installed and COM is working")
 571        sys.exit()
 572
 573    # Continue because we can connect to PowerPoint
 574    logger.debug("Found PowerPoint application")
 575
 576    # Dispatch events
 577    win32com.client.WithEvents(ppt_application, ApplicationEvents)
 578    logger.debug("Dispatched events")
 579
 580    # Deal with windows in protected view
 581    manage_protected_view(ppt_application)
 582
 583    # Initial enumeration of open presentations
 584    logger.debug("Enumerating {} presentation(s)".format(len(ppt_application.Presentations)))
 585    for n in range(1, len(ppt_application.Presentations)+1):    # PowerPoint's slide indexing starts at 1.. why!?!?!?
 586        pres = Presentation(ppt_application, n)
 587        icon.notify("Connected to {}".format(pres.presentation.Name), PKG_NAME)
 588        logger.debug("Found presentation {} with index {}".format(pres.presentation.Name, n))
 589        ppt_presentations[pres.presentation.Name] = pres
 590    refresh_daemon = threading.Thread(name="refresh_daemon", target=refresh)
 591    refresh_daemon.setDaemon(True)
 592    logger.debug("Handing over to refresh daemon - goodbye...")
 593    if len(ppt_presentations) == 0:
 594        # Provide some confirmation that the program has started if we haven't sent any 
 595        # connection notifications yet
 596        icon.notify("Started server", PKG_NAME)
 597    refresh_daemon.start()
 598
 599
 600def start_server(_=None):
 601    """
 602    Start HTTP and WS servers, then connect to PPT instance with connect_ppt() which will then 
 603    set off the refresh daemon.
 604    """
 605    setup_http()
 606    setup_ws()
 607    connect_ppt()
 608        
 609        
 610def exit(icon):
 611    """
 612    Clean up and exit when user clicks "Stop" from systray menu
 613    """
 614    logger.debug("User requested shutdown")
 615    icon.visible = False
 616    icon.stop()
 617
 618
 619def start_interface():
 620    """
 621    Main entrypoint for the program. Loads config and logging, starts systray icon, and calls
 622    start_server() to start the backend.
 623    """
 624    global icon
 625    global logger
 626    # Load config
 627    config.prefs = config.loadconf(CONFIG_FILE)
 628
 629    # Set up logging
 630    if config.prefs["Main"]["logging"] == "debug":
 631        log_level = logging.DEBUG
 632    elif config.prefs["Main"]["logging"] == "info":
 633        log_level = logging.CRITICAL
 634    else:
 635        log_level = logging.WARNING
 636    log_level = logging.DEBUG
 637
 638    log_formatter = logging.Formatter("%(asctime)s [%(threadName)-12.12s] [%(levelname)-7.7s]  %(message)s")
 639    logger = logging.getLogger("ppt-control")
 640    logger.setLevel(log_level)
 641    logger.propagate = False
 642
 643    file_handler = logging.FileHandler("{0}/{1}".format(os.getenv("APPDATA"), LOGFILE))
 644    file_handler.setFormatter(log_formatter)
 645    file_handler.setLevel(log_level)
 646    logger.addHandler(file_handler)
 647
 648    console_handler = logging.StreamHandler()
 649    console_handler.setFormatter(log_formatter)
 650    console_handler.setLevel(log_level)
 651    logger.addHandler(console_handler)
 652
 653    #logging.getLogger("asyncio").setLevel(logging.ERROR)
 654    #logging.getLogger("asyncio.coroutines").setLevel(logging.ERROR)
 655    logging.getLogger("websockets.server").setLevel(logging.ERROR)
 656    #logging.getLogger("websockets.protocol").setLevel(logging.ERROR)
 657
 658
 659    logger.debug("Finished setting up config and logging")
 660
 661    # Start systray icon and server
 662    logger.debug("Starting system tray icon")
 663    icon = pystray.Icon(PKG_NAME)
 664    icon.icon = Image.open(os.path.dirname(os.path.realpath(__file__)) + r'''\static\icons\ppt.ico''')
 665    icon.title = "{} {}".format(PKG_NAME, PKG_VERSION)
 666    #refresh_menu(icon)
 667    icon.visible = True
 668    icon.run(setup=start_server)
 669
 670    # Exit when icon has stopped
 671    sys.exit(0)
 672
 673if __name__ == "__main__":
 674    start_interface()