Example #1
0
class PiPresents(object):
    def pipresents_version(self):
        vitems = self.pipresents_issue.split('.')
        if len(vitems) == 2:
            # cope with 2 digit version numbers before 1.3.2
            return 1000 * int(vitems[0]) + 100 * int(vitems[1])
        else:
            return 1000 * int(vitems[0]) + 100 * int(vitems[1]) + int(
                vitems[2])

    def __init__(self):
        # gc.set_debug(gc.DEBUG_UNCOLLECTABLE|gc.DEBUG_INSTANCES|gc.DEBUG_OBJECTS|gc.DEBUG_SAVEALL)
        gc.set_debug(gc.DEBUG_UNCOLLECTABLE | gc.DEBUG_SAVEALL)
        self.pipresents_issue = "1.4.6"
        self.pipresents_minorissue = '1.4.6c'

        StopWatch.global_enable = False

        # set up the handler for SIGTERM
        signal.signal(signal.SIGTERM, self.handle_sigterm)

        # ****************************************
        # Initialisation
        # ***************************************
        # get command line options
        self.options = command_options()
        # print (self.options)

        # get Pi Presents code directory
        pp_dir = sys.path[0]
        self.pp_dir = pp_dir

        if not os.path.exists(pp_dir + "/pipresents.py"):
            if self.options['manager'] is False:
                tkinter.messagebox.showwarning("Pi Presents",
                                               "Bad Application Directory")
            exit(102)

        # Initialise logging and tracing

        # initlize VLCDriver logger
        self.logger = Logger(enabled=True)
        self.logger.init()
        # TURN OFF REST OF LOGGING IN pp_vlcdriver.py

        # Init main monitor
        Monitor.log_path = pp_dir
        self.mon = Monitor()
        # Init in PiPresents only
        self.mon.init()

        # uncomment to enable control of logging from within a class
        # Monitor.enable_in_code = True # enables control of log level in the code for a class  - self.mon.set_log_level()

        # make a shorter list to log/trace only some classes without using enable_in_code.
        Monitor.classes = [
            'PiPresents', 'HyperlinkShow', 'RadioButtonShow', 'ArtLiveShow',
            'ArtMediaShow', 'MediaShow', 'LiveShow', 'MenuShow', 'GapShow',
            'Show', 'ArtShow', 'AudioPlayer', 'BrowserPlayer', 'ImagePlayer',
            'MenuPlayer', 'MessagePlayer', 'VideoPlayer', 'Player',
            'VLCPlayer', 'ChromePlayer', 'MediaList', 'LiveList', 'ShowList',
            'PathManager', 'ControlsManager', 'ShowManager',
            'TrackPluginManager', 'IOPluginManager', 'MplayerDriver',
            'OMXDriver', 'UZBLDriver', 'TimeOfDay', 'ScreenDriver', 'Animate',
            'OSCDriver', 'CounterManager', 'Network', 'Mailer'
        ]

        # Monitor.classes=['PiPresents','MediaShow','GapShow','Show','VideoPlayer','Player','OMXDriver']
        # Monitor.classes=['OSCDriver']

        # get global log level from command line
        Monitor.log_level = int(self.options['debug'])
        Monitor.manager = self.options['manager']
        # print self.options['manager']
        self.mon.newline(3)
        self.mon.sched(
            self, None,
            "Pi Presents is starting, Version:" + self.pipresents_minorissue +
            ' at ' + time.strftime("%Y-%m-%d %H:%M.%S"))
        self.mon.log(
            self,
            "Pi Presents is starting, Version:" + self.pipresents_minorissue +
            ' at ' + time.strftime("%Y-%m-%d %H:%M.%S"))
        # self.mon.log (self," OS and separator:" + os.name +'  ' + os.sep)
        self.mon.log(self, "sys.path[0] -  location of code: " + sys.path[0])

        # log versions of Raspbian and omxplayer, and GPU Memory
        with open("/boot/issue.txt") as ifile:
            self.mon.log(self, '\nRaspbian: ' + ifile.read())

        self.mon.log(
            self,
            '\n' + check_output(["omxplayer", "-v"], universal_newlines=True))
        self.mon.log(
            self,
            '\nGPU Memory: ' + check_output(["vcgencmd", "get_mem", "gpu"],
                                            universal_newlines=True))

        if os.geteuid() == 0:
            print('Do not run Pi Presents with sudo')
            self.mon.log(self, 'Do not run Pi Presents with sudo')
            self.mon.finish()
            sys.exit(102)

        if "DESKTOP_SESSION" not in os.environ:
            print('Pi Presents must be run from the Desktop')
            self.mon.log(self, 'Pi Presents must be run from the Desktop')
            self.mon.finish()
            sys.exit(102)
        else:
            self.mon.log(self, 'Desktop is ' + os.environ['DESKTOP_SESSION'])

        # optional other classes used
        self.root = None
        self.ppio = None
        self.tod = None
        self.dm = None
        self.animate = None
        self.ioplugin_manager = None
        self.oscdriver = None
        self.osc_enabled = False
        self.tod_enabled = False
        self.email_enabled = False

        user = os.getenv('USER')

        if user is None:
            tkinter.messagebox.showwarning(
                "You must be logged in to run Pi Presents")
            exit(102)

        if user != 'pi':
            self.mon.warn(self, "You must be logged as pi to use GPIO")

        self.mon.log(self, 'User is: ' + user)
        # self.mon.log(self,"os.getenv('HOME') -  user home directory (not used): " + os.getenv('HOME')) # does not work
        # self.mon.log(self,"os.path.expanduser('~') -  user home directory: " + os.path.expanduser('~'))   # does not work

        # check network is available
        self.network_connected = False
        self.network_details = False
        self.interface = ''
        self.ip = ''
        self.unit = ''

        # sets self.network_connected and self.network_details
        self.init_network()

        # start the mailer and send email when PP starts
        self.email_enabled = False
        if self.network_connected is True:
            self.init_mailer()
            if self.email_enabled is True and self.mailer.email_at_start is True:
                subject = '[Pi Presents] ' + self.unit + ': PP Started on ' + time.strftime(
                    "%Y-%m-%d %H:%M")
                message = time.strftime(
                    "%Y-%m-%d %H:%M"
                ) + '\nUnit: ' + self.unit + '   Profile: ' + self.options[
                    'profile'] + '\n ' + self.interface + '\n ' + self.ip
                self.send_email('start', subject, message)

        # get profile path from -p option
        if self.options['profile'] != '':
            self.pp_profile_path = "/pp_profiles/" + self.options['profile']
        else:
            self.mon.err(self, "Profile not specified in command ")
            self.end('error',
                     'Profile not specified with the commands -p option')

    # get directory containing pp_home from the command,
        if self.options['home'] == "":
            home = os.sep + 'home' + os.sep + user + os.sep + "pp_home"
        else:
            home = self.options['home'] + os.sep + "pp_home"
        self.mon.log(self, "pp_home directory is: " + home)

        # check if pp_home exists.
        # try for 10 seconds to allow usb stick to automount
        found = False
        for i in range(1, 10):
            self.mon.log(self,
                         "Trying pp_home at: " + home + " (" + str(i) + ')')
            if os.path.exists(home):
                found = True
                self.pp_home = home
                break
            time.sleep(1)
        if found is True:
            self.mon.log(
                self,
                "Found Requested Home Directory, using pp_home at: " + home)
        else:
            self.mon.err(self, "Failed to find pp_home directory at " + home)
            self.end('error', "Failed to find pp_home directory at " + home)

        # check profile exists
        self.pp_profile = self.pp_home + self.pp_profile_path
        if os.path.exists(self.pp_profile):
            self.mon.sched(self, None,
                           "Running profile: " + self.pp_profile_path)
            self.mon.log(
                self, "Found Requested profile - pp_profile directory is: " +
                self.pp_profile)
        else:
            self.mon.err(
                self, "Failed to find requested profile: " + self.pp_profile)
            self.end('error',
                     "Failed to find requested profile: " + self.pp_profile)

        self.mon.start_stats(self.options['profile'])

        # initialise and read the showlist in the profile
        self.showlist = ShowList()
        self.showlist_file = self.pp_profile + "/pp_showlist.json"
        if os.path.exists(self.showlist_file):
            self.showlist.open_json(self.showlist_file)
        else:
            self.mon.err(self, "showlist not found at " + self.showlist_file)
            self.end('error', "showlist not found at " + self.showlist_file)

        # check profile and Pi Presents issues are compatible
        if self.showlist.profile_version() != self.pipresents_version():
            self.mon.err(
                self,
                "Version of showlist " + self.showlist.profile_version_string +
                " is not  same as Pi Presents")
            self.end(
                'error',
                "Version of showlist " + self.showlist.profile_version_string +
                " is not  same as Pi Presents")

        # get the 'start' show from the showlist
        index = self.showlist.index_of_start_show()
        if index >= 0:
            self.showlist.select(index)
            self.starter_show = self.showlist.selected_show()
        else:
            self.mon.err(self, "Show [start] not found in showlist")
            self.end('error', "Show [start] not found in showlist")

# ********************
# SET UP THE GUI
# ********************
# turn off the screenblanking and saver
        if self.options['noblank'] is True:
            call(["xset", "s", "off"])
            call(["xset", "s", "-dpms"])

        # find connected displays and create a canvas for each display
        self.dm = DisplayManager()
        status, message, self.root = self.dm.init(self.options,
                                                  self.handle_user_abort,
                                                  self.pp_dir, False)
        if status != 'normal':
            self.mon.err(self, message)
            self.end('error', message)

        self.mon.log(
            self,
            str(DisplayManager.num_displays) + ' Displays are connected:')

        for display_id in DisplayManager.displays:
            if self.dm.has_canvas(display_id):
                canvas_obj = self.dm.canvas_widget(display_id)
                canvas_obj.config(bg=self.starter_show['background-colour'])
            name = self.dm.name_of_display(display_id)
            width, height = self.dm.real_display_dimensions(display_id)
            x, y = self.dm.real_display_position(display_id)
            matrix, ms = self.dm.touch_matrix_for(display_id)
            rotation = self.dm.real_display_orientation(display_id)
            self.mon.log(
                self, '   - ' + name + ' Id: ' + str(display_id) + ' ' +
                str(x) + '+' + str(y) + '+' + str(width) + '*' + str(height) +
                ' ' + rotation)
            self.mon.log(self, '     ' + ms)

            status, message, driver_name = self.dm.get_driver_name(display_id)
            if status == 'normal':
                self.mon.log(self,
                             name + ':  Touch Driver: ' + driver_name + '\n')
            elif status == 'null':
                self.mon.log(self, name + ':  Touch Driver not Defined\n')
            else:
                self.mon.err(self, message)

# ****************************************
# INITIALISE THE TOUCHSCREEN DRIVER
# ****************************************

# each driver takes a set of inputs, binds them to symboic names
# and sets up a callback which returns the symbolic name when an input event occurs

        self.sr = ScreenDriver()
        # read the screen click area config file
        reason, message = self.sr.read(pp_dir, self.pp_home, self.pp_profile)
        if reason == 'error':
            self.end('error', 'cannot find, or error in screen.cfg')

        # create click areas on the canvases, must be polygon as outline rectangles are not filled as far as find_closest goes
        reason, message = self.sr.make_click_areas(self.handle_input_event)
        if reason == 'error':
            self.mon.err(self, message)
            self.end('error', message)

# ****************************************
# INITIALISE THE APPLICATION AND START
# ****************************************
        self.shutdown_required = False
        self.reboot_required = False
        self.terminate_required = False
        self.exitpipresents_required = False
        self.restartpipresents_required = False

        #initialise the Audio manager
        self.audiomanager = AudioManager()
        status, message = self.audiomanager.init(self.pp_dir)
        if status == 'error':
            self.mon.err(self, message)
            self.end('error', message)

        # initialise the Beeps Player
        self.bp = BeepPlayer()
        self.bp.init(self.pp_home, self.pp_profile)

        # initialise the I/O plugins by importing their drivers
        self.ioplugin_manager = IOPluginManager()
        reason, message = self.ioplugin_manager.init(self.pp_dir,
                                                     self.pp_profile,
                                                     self.root,
                                                     self.handle_input_event,
                                                     self.pp_home)
        if reason == 'error':
            self.mon.err(self, message)
            self.end('error', message)

        # kick off animation sequencer
        self.animate = Animate()
        self.animate.init(pp_dir, self.pp_home, self.pp_profile, self.root,
                          200, self.handle_output_event)
        self.animate.poll()

        #create a showmanager ready for time of day scheduler and osc server
        show_id = -1
        self.show_manager = ShowManager(show_id, self.showlist,
                                        self.starter_show, self.root,
                                        self.pp_dir, self.pp_profile,
                                        self.pp_home)
        # first time through set callback to terminate Pi Presents if all shows have ended.
        self.show_manager.init(self.all_shows_ended_callback,
                               self.handle_command, self.showlist)
        # Register all the shows in the showlist
        reason, message = self.show_manager.register_shows()
        if reason == 'error':
            self.mon.err(self, message)
            self.end('error', message)

        # Init OSCDriver, read config and start OSC server
        self.osc_enabled = False
        if self.network_connected is True:
            if os.path.exists(self.pp_profile + os.sep + 'pp_io_config' +
                              os.sep + 'osc.cfg'):
                self.oscdriver = OSCDriver()
                reason, message = self.oscdriver.init(
                    self.pp_profile, self.unit, self.interface, self.ip,
                    self.handle_command, self.handle_input_event,
                    self.e_osc_handle_animate)
                if reason == 'error':
                    self.mon.err(self, message)
                    self.end('error', message)
                else:
                    self.osc_enabled = True
                    self.root.after(1000, self.oscdriver.start_server())

        # initialise ToD scheduler calculating schedule for today
        self.tod = TimeOfDay()
        reason, message, self.tod_enabled = self.tod.init(
            pp_dir, self.pp_home, self.pp_profile, self.showlist, self.root,
            self.handle_command)
        if reason == 'error':
            self.mon.err(self, message)
            self.end('error', message)

        # warn if the network not available when ToD required
        if self.tod_enabled is True and self.network_connected is False:
            self.mon.warn(
                self,
                'Network not connected  so Time of Day scheduler may be using the internal clock'
            )

        # init the counter manager
        self.counter_manager = CounterManager()
        if self.starter_show['counters-store'] == 'yes':
            store_enable = True
        else:
            store_enable = False
        reason, message = self.counter_manager.init(
            self.pp_profile + '/counters.cfg', store_enable,
            self.options['loadcounters'],
            self.starter_show['counters-initial'])
        if reason == 'error':
            self.mon.err(self, message)
            self.end('error', message)

        # warn about start shows and scheduler

        if self.starter_show['start-show'] == '' and self.tod_enabled is False:
            self.mon.sched(
                self, None,
                "No Start Shows in Start Show and no shows scheduled")
            self.mon.warn(
                self, "No Start Shows in Start Show and no shows scheduled")

        if self.starter_show['start-show'] != '' and self.tod_enabled is True:
            self.mon.sched(
                self, None,
                "Start Shows in Start Show and shows scheduled - conflict?")
            self.mon.warn(
                self,
                "Start Shows in Start Show and shows scheduled - conflict?")

        # run the start shows
        self.run_start_shows()

        # kick off the time of day scheduler which may run additional shows
        if self.tod_enabled is True:
            self.tod.poll()

        # start the I/O plugins input event generation
        self.ioplugin_manager.start()

        # start Tkinters event loop
        self.root.mainloop()

# *********************
#  RUN START SHOWS
# ********************

    def run_start_shows(self):
        self.mon.trace(self, 'run start shows')
        # parse the start shows field and start the initial shows
        show_refs = self.starter_show['start-show'].split()
        for show_ref in show_refs:
            reason, message = self.show_manager.control_a_show(
                show_ref, 'open')
            if reason == 'error':
                self.mon.err(self, message)
                self.terminate()
                #self.end(reason,message)

# *********************
# User inputs
# ********************

    def e_osc_handle_animate(self, line):
        #jump  out of server thread
        self.root.after(1, lambda arg=line: self.osc_handle_animate(arg))

    def osc_handle_animate(self, line):
        self.mon.log(self, "animate command received: " + line)
        #osc sends output events as a string
        reason, message, delay, name, param_type, param_values = self.animate.parse_animate_fields(
            line)
        if reason == 'error':
            self.mon.err(self, message)
            self.end(reason, message)
        self.handle_output_event(name, param_type, param_values, 0)

    def show_control_handle_animate(self, line):
        self.mon.log(self, "animate show control command received: " + line)
        line = '0 ' + line
        reason, message, delay, name, param_type, param_values = self.animate.parse_animate_fields(
            line)
        # print (reason,message,delay,name,param_type,param_values)
        if reason == 'error':
            self.mon.err(self, message)
            self.end(reason, message)
        self.handle_output_event(name, param_type, param_values, 0)

    # output events are animate commands
    def handle_output_event(self, symbol, param_type, param_values, req_time):
        reason, message = self.ioplugin_manager.handle_output_event(
            symbol, param_type, param_values, req_time)
        if reason == 'error':
            self.mon.err(self, message)
            self.end(reason, message)

    # all input events call this callback providing a symbolic name.
    # handle events that affect PP overall, otherwise pass to all active shows
    def handle_input_event(self, symbol, source):
        self.mon.log(self, "event received: " + symbol + ' from ' + source)
        if symbol == 'pp-terminate':
            self.handle_user_abort()

        elif symbol == 'pp-shutdown':
            self.mon.err(
                self,
                'pp-shutdown removed in version 1.3.3a, see Release Notes')
            self.end(
                'error',
                'pp-shutdown removed in version 1.3.3a, see Release Notes')

        elif symbol == 'pp-shutdownnow':
            # need root.after to get out of st thread
            self.root.after(1, self.shutdownnow_pressed)
            return

        elif symbol == 'pp-exitpipresents':
            self.exitpipresents_required = True
            if self.show_manager.all_shows_exited() is True:
                # need root.after to grt out of st thread
                self.root.after(1, self.e_all_shows_ended_callback)
                return
            reason, message = self.show_manager.exit_all_shows()

        elif symbol == 'pp-restartpipresents':
            self.restartpipresents_required = True
            if self.show_manager.all_shows_exited() is True:
                # need root.after to grt out of st thread
                self.root.after(1, self.e_all_shows_ended_callback)
                return
            reason, message = self.show_manager.exit_all_shows()

        else:
            # pass the input event to all registered shows
            for show in self.show_manager.shows:
                show_obj = show[ShowManager.SHOW_OBJ]
                if show_obj is not None:
                    show_obj.handle_input_event(symbol)

    # commands are generated by tracks and shows
    # they can open or close shows, generate input events and do special tasks
    # commands also generate osc outputs to other computers
    # handles one command provided as a line of text

    def handle_command(self, command_text, source='', show=''):
        # print 'PIPRESENTS ',command_text,'\n   Source',source,'from',show
        self.mon.log(self, "command received: " + command_text)
        if command_text.strip() == "":
            return

        fields = command_text.split()

        if fields[0] in ('osc', 'OSC'):
            if self.osc_enabled is True:
                status, message = self.oscdriver.parse_osc_command(fields[1:])
                if status == 'warn':
                    self.mon.warn(self, message)
                if status == 'error':
                    self.mon.err(self, message)
                    self.end('error', message)
                return
            else:
                return

        if fields[0] == 'counter':
            status, message = self.counter_manager.parse_counter_command(
                fields[1:])
            if status == 'error':
                self.mon.err(self, message)
                self.end('error', message)
            return

        if fields[0] == 'beep':
            status, message = self.bp.play_show_beep(command_text)
            if status == 'error':
                self.mon.err(self, message)
                self.end('error', message)
                return
            return

        if fields[0] == 'backlight':
            # on, off, inc val, dec val, set val fade val duration
            status, message = self.dm.do_backlight_command(command_text)
            if status == 'error':
                self.mon.err(self, message)
                self.end('error', message)
                return
            return

        if fields[0] == 'monitor':
            status, message = self.dm.handle_monitor_command(fields[1:])
            if status == 'error':
                self.mon.err(self, message)
                self.end('error', message)
                return
            return

        if fields[0] == 'cec':
            status, message = self.handle_cec_command(fields[1:])
            if status == 'error':
                self.mon.err(self, message)
                self.end('error', message)
                return
            return

        if fields[0] == 'animate':
            self.show_control_handle_animate(' '.join(fields[1:]))
            return

        # show commands
        show_command = fields[0]
        if len(fields) > 1:
            show_ref = fields[1]
        else:
            show_ref = ''

        if show_command in ('open', 'close', 'closeall', 'openexclusive'):
            self.mon.sched(self, TimeOfDay.now,
                           command_text + ' received from show:' + show)
            if self.shutdown_required is False and self.terminate_required is False:
                reason, message = self.show_manager.control_a_show(
                    show_ref, show_command)
            else:
                return

        elif show_command == 'event':
            self.handle_input_event(show_ref, 'Show Control')
            return

        elif show_command == 'exitpipresents':
            self.exitpipresents_required = True
            if self.show_manager.all_shows_exited() is True:
                # need root.after to get out of st thread
                self.root.after(1, self.e_all_shows_ended_callback)
                return
            else:
                reason, message = self.show_manager.exit_all_shows()

        elif show_command == 'shutdownnow':
            # need root.after to get out of st thread
            self.root.after(1, self.shutdownnow_pressed)
            return

        elif show_command == 'reboot':
            # need root.after to get out of st thread
            self.root.after(1, self.reboot_pressed)
            return

        else:
            reason = 'error'
            message = 'command not recognised: ' + show_command

        if reason == 'error':
            self.mon.err(self, message)
            self.end('error', message)
            return
        return

    def handle_cec_command(self, args):
        if len(args) == 0:
            return 'error', 'no arguments for CEC command'

        if len(args) == 1:
            device = '0'
            if args[0] == 'scan':
                com = 'echo scan | cec-client -s -d 1'
                #print (com)
                os.system(com)
                return 'normal', ''

            if args[0] == 'as':
                com = 'echo as | cec-client -s -d 1'
                #print (com)
                os.system(com)
                return 'normal', ''

        if len(args) == 2:
            device = args[1]
            if not device.isdigit():
                return 'error', 'device is not a positive integer'

        if args[0] == 'on':
            com = 'echo "on ' + device + '" | cec-client -s -d 1'
            #print (com)
            os.system(com)
            return 'normal', ''
        elif args[0] == 'standby':
            com = 'echo "standby ' + device + '" | cec-client -s -d 1'
            #print (com)
            os.system(com)
            return 'normal', ''
        else:
            return 'error', 'Unknown CEC command: ' + args[0]

    # deal with differnt commands/input events

    def shutdownnow_pressed(self):
        self.shutdown_required = True
        if self.show_manager.all_shows_exited() is True:
            self.all_shows_ended_callback('normal', 'no shows running')
        else:
            # calls exit method of all shows, results in all_shows_closed_callback
            self.show_manager.exit_all_shows()

    def reboot_pressed(self):
        self.reboot_required = True
        if self.show_manager.all_shows_exited() is True:
            self.all_shows_ended_callback('normal', 'no shows running')
        else:
            # calls exit method of all shows, results in all_shows_closed_callback
            self.show_manager.exit_all_shows()

    def handle_sigterm(self, signum, fframe):
        self.mon.log(self, 'SIGTERM received - ' + str(signum))
        self.terminate()

    def handle_user_abort(self):
        self.mon.log(self, 'User abort received')
        self.terminate()

    def terminate(self):
        self.mon.log(self, "terminate received")
        self.terminate_required = True
        needs_termination = False
        for show in self.show_manager.shows:
            # print  show[ShowManager.SHOW_OBJ], show[ShowManager.SHOW_REF]
            if show[ShowManager.SHOW_OBJ] is not None:
                needs_termination = True
                self.mon.log(
                    self,
                    "Sent terminate to show " + show[ShowManager.SHOW_REF])
                # call shows terminate method
                # eventually the show will exit and after all shows have exited all_shows_callback will be executed.
                show[ShowManager.SHOW_OBJ].terminate()
        if needs_termination is False:
            self.end('killed', 'killed - no termination of shows required')

# ******************************
# Ending Pi Presents after all the showers and players are closed
# **************************

    def e_all_shows_ended_callback(self):
        self.all_shows_ended_callback('normal', 'no shows running')

    # callback from ShowManager when all shows have ended
    def all_shows_ended_callback(self, reason, message):
        for display_name in DisplayManager.display_map:
            status, message, display_id, canvas_obj = self.dm.id_of_canvas(
                display_name)
            if status != 'normal':
                continue
            canvas_obj.config(bg=self.starter_show['background-colour'])
        if reason in (
                'killed', 'error'
        ) or self.shutdown_required is True or self.exitpipresents_required is True or self.reboot_required is True or self.restartpipresents_required is True:
            self.end(reason, message)

    def end(self, reason, message):
        self.mon.log(self, "Pi Presents ending with reason: " + reason)
        if self.root is not None:
            self.root.destroy()
        self.tidy_up()

        if reason == 'killed':
            if self.email_enabled is True and self.mailer.email_on_terminate is True:
                subject = '[Pi Presents] ' + self.unit + ': PP Exited with reason: Terminated'
                message = time.strftime(
                    "%Y-%m-%d %H:%M"
                ) + '\n ' + self.unit + '\n ' + self.interface + '\n ' + self.ip
                self.send_email(reason, subject, message)
            self.mon.sched(self, None, "Pi Presents Terminated, au revoir\n")
            self.mon.log(self, "Pi Presents Terminated, au revoir")

            # close logging files
            self.mon.finish()
            #print('Uncollectable Garbage',gc.collect())
            # objgraph.show_backrefs(objgraph.by_type('Canvas'),filename='backrefs.png')
            sys.exit(101)

        elif reason == 'error':
            if self.email_enabled is True and self.mailer.email_on_error is True:
                subject = '[Pi Presents] ' + self.unit + ': PP Exited with reason: Error'
                message_text = 'Download log for error message\n' + time.strftime(
                    "%Y-%m-%d %H:%M"
                ) + '\n ' + self.unit + '\n ' + self.interface + '\n ' + self.ip
                self.send_email(reason, subject, message_text)
            self.mon.sched(self, None,
                           "Pi Presents closing because of error, sorry\n")
            self.mon.log(self, "Pi Presents closing because of error, sorry")

            # close logging files
            self.mon.finish()
            #print('uncollectable garbage',gc.collect())
            sys.exit(102)

        else:
            self.mon.sched(self, None, "Pi Presents  exiting normally, bye\n")
            self.mon.log(self, "Pi Presents  exiting normally, bye")

            # close logging files
            self.mon.finish()
            if self.restartpipresents_required is True:
                #print ('restart')
                return

            if self.reboot_required is True:
                # print 'REBOOT'
                call(['sudo', 'reboot'])
            if self.shutdown_required is True:
                # print 'SHUTDOWN'
                call(['sudo', 'shutdown', 'now', 'SHUTTING DOWN'])
            #print('uncollectable garbage',gc.collect())
            sys.exit(100)

    # tidy up all the peripheral bits of Pi Presents
    def tidy_up(self):
        self.mon.log(self, "Tidying Up")
        # backlight
        if self.dm != None:
            self.dm.terminate()
        # turn screen blanking back on
        if self.options['noblank'] is True:
            call(["xset", "s", "on"])
            call(["xset", "s", "+dpms"])

        # tidy up animation
        if self.animate is not None:
            self.animate.terminate()

        # tidy up i/o plugins
        if self.ioplugin_manager != None:
            self.ioplugin_manager.terminate()

        if self.osc_enabled is True:
            self.oscdriver.terminate()

        # tidy up time of day scheduler
        if self.tod_enabled is True:
            self.tod.terminate()


# *******************************
# Connecting to network and email
# *******************************

    def init_network(self):

        timeout = int(self.options['nonetwork'])
        if timeout == 0:
            self.network_connected = False
            self.unit = ''
            self.ip = ''
            self.interface = ''
            return

        self.network = Network()
        self.network_connected = False

        # try to connect to network
        self.mon.log(self,
                     'Waiting up to ' + str(timeout) + ' seconds for network')
        success = self.network.wait_for_network(timeout)
        if success is False:
            self.mon.warn(
                self, 'Failed to connect to network after ' + str(timeout) +
                ' seconds')
            # tkMessageBox.showwarning("Pi Presents","Failed to connect to network so using fake-hwclock")
            return

        self.network_connected = True
        self.mon.sched(
            self, None, 'Time after network check is ' +
            time.strftime("%Y-%m-%d %H:%M.%S"))
        self.mon.log(
            self, 'Time after network check is ' +
            time.strftime("%Y-%m-%d %H:%M.%S"))

        # Get web configuration
        self.network_details = False
        network_options_file_path = self.pp_dir + os.sep + 'pp_config' + os.sep + 'pp_web.cfg'
        if not os.path.exists(network_options_file_path):
            self.mon.warn(
                self, "pp_web.cfg not found at " + network_options_file_path)
            return
        self.mon.log(self, 'Found pp_web.cfg in ' + network_options_file_path)

        self.network.read_config(network_options_file_path)
        self.unit = self.network.unit

        # get interface and IP details of preferred interface
        self.interface, self.ip = self.network.get_preferred_ip()
        if self.interface == '':
            self.network_connected = False
            return
        self.network_details = True
        self.mon.log(
            self, 'Network details ' + self.unit + ' ' + self.interface + ' ' +
            self.ip)

    def init_mailer(self):

        self.email_enabled = False
        email_file_path = self.pp_dir + os.sep + 'pp_config' + os.sep + 'pp_email.cfg'
        if not os.path.exists(email_file_path):
            self.mon.log(self, 'pp_email.cfg not found at ' + email_file_path)
            return
        self.mon.log(self, 'Found pp_email.cfg at ' + email_file_path)
        self.mailer = Mailer()
        self.mailer.read_config(email_file_path)
        # all Ok so can enable email if config file allows it.
        if self.mailer.email_allowed is True:
            self.email_enabled = True
            self.mon.log(self, 'Email Enabled')

    def try_connect(self):
        tries = 1
        while True:
            success, error = self.mailer.connect()
            if success is True:
                return True
            else:
                self.mon.log(
                    self, 'Failed to connect to email SMTP server ' +
                    str(tries) + '\n ' + str(error))
                tries += 1
                if tries > 5:
                    self.mon.log(
                        self, 'Failed to connect to email SMTP server after ' +
                        str(tries))
                    return False

    def send_email(self, reason, subject, message):
        if self.try_connect() is False:
            return False
        else:
            success, error = self.mailer.send(subject, message)
            if success is False:
                self.mon.log(self, 'Failed to send email: ' + str(error))
                success, error = self.mailer.disconnect()
                if success is False:
                    self.mon.log(self,
                                 'Failed disconnect after send:' + str(error))
                return False
            else:
                self.mon.log(self, 'Sent email for ' + reason)
                success, error = self.mailer.disconnect()
                if success is False:
                    self.mon.log(
                        self,
                        'Failed disconnect from email server ' + str(error))
                return True
Example #2
0
class ChromePlayer(Player):

    # ***************************************
    # EXTERNAL COMMANDS
    # ***************************************

    def __init__(self, show_id, showlist, root, canvas, show_params,
                 track_params, pp_dir, pp_home, pp_profile, end_callback,
                 command_callback):

        # initialise items common to all players
        Player.__init__(self, show_id, showlist, root, canvas, show_params,
                        track_params, pp_dir, pp_home, pp_profile,
                        end_callback, command_callback)

        self.mon.trace(self, '')

        # and initialise things for this player
        self.dm = DisplayManager()

        # get duration limit (secs ) from profile
        if self.track_params['duration'] != '':
            self.duration_text = self.track_params['duration']
        else:
            self.duration_text = self.show_params['duration']

        # process chrome window
        if self.track_params['chrome-window'] != '':
            self.chrome_window_text = self.track_params['chrome-window']
        else:
            self.chrome_window_text = self.show_params['chrome-window']

        # process chrome things
        if self.track_params['chrome-freeze-at-end'] != '':
            self.freeze_at_end = self.track_params['chrome-freeze-at-end']
        else:
            self.freeze_at_end = self.show_params['chrome-freeze-at-end']

        if self.track_params['chrome-zoom'] != '':
            self.chrome_zoom_text = self.track_params['chrome-zoom']
        else:
            self.chrome_zoom_text = self.show_params['chrome-zoom']

        if self.track_params['chrome-other-options'] != '':
            self.chrome_other_options = self.track_params[
                'chrome-other-options']
        else:
            self.chrome_other_options = self.show_params[
                'chrome-other-options']

        # Initialize variables
        self.command_timer = None
        self.tick_timer = None
        self.quit_signal = False  # signal that user has pressed stop

        # initialise the play state
        self.play_state = 'initialised'
        self.load_state = ''

    # LOAD - loads the browser and show stuff
    def load(self, track, loaded_callback, enable_menu):
        # instantiate arguments
        self.loaded_callback = loaded_callback  # callback when loaded
        self.mon.trace(self, '')

        status, message, duration100 = Player.parse_duration(
            self.duration_text)
        if status == 'error':
            self.mon.err(self, message)
            self.play_state = 'load-failed'
            if self.loaded_callback is not None:
                self.loaded_callback('error', message)
                return
        self.duration = 2 * duration100

        # Is display valid and connected
        status, message, self.display_id = self.dm.id_of_display(
            self.show_canvas_display_name)
        if status == 'error':
            self.mon.err(self, message)
            self.play_state = 'load-failed'
            if self.loaded_callback is not None:
                self.loaded_callback('error', 'cannot find file; ' + track)
                return

        # does media exist
        if not ':' in track:
            if not os.path.exists(track):
                self.mon.err(self, 'cannot find file; ' + track)
                self.play_state = 'load-failed'
                if self.loaded_callback is not None:
                    self.loaded_callback('error', 'cannot find file; ' + track)
                    return

        # add file:// to files.
        if ':' in track:
            self.current_url = track
        else:
            self.current_url = 'file://' + track
        # do common bits of  load
        Player.pre_load(self)

        # prepare chromium options
        status, message = self.process_chrome_options()
        if status == 'error':
            self.mon.err(self, message)
            self.play_state = 'load-failed'
            if self.loaded_callback is not None:
                self.loaded_callback('error', message)
                return

        # parse browser commands to self.command_list
        reason, message = self.parse_commands(
            self.track_params['browser-commands'])
        if reason == 'error':
            self.mon.err(self, message)
            self.play_state = 'load-failed'
            if self.loaded_callback is not None:
                self.loaded_callback('error', message)
                return

        # load the plugin, this may modify self.track and enable the plugin drawing to canvas
        if self.track_params['plugin'] != '':
            status, message = self.load_plugin()
            if status == 'error':
                self.mon.err(self, message)
                self.play_state = 'load-failed'
                if self.loaded_callback is not None:
                    self.loaded_callback('error', message)
                    return

        # start loading the browser
        self.play_state = 'loading'

        # load the images and text
        status, message = self.load_x_content(enable_menu)
        if status == 'error':
            self.mon.err(self, message)
            self.play_state = 'load-failed'
            if self.loaded_callback is not None:
                self.loaded_callback('error', message)
                return

        #start the browser
        self.driver_open()

        # for kiosk and fullscreen need to get the url - in browser command for app mode
        if self.app_mode is False:
            self.driver_get(self.current_url)
        self.mon.log(self,
                     'Loading browser from show Id: ' + str(self.show_id))

        self.play_state = 'loaded'

        # and start executing the browser commands
        self.play_commands()
        self.mon.log(self, "      State machine: chromium loaded")
        if self.loaded_callback is not None:
            self.loaded_callback('normal', 'browser loaded')
        return

    # UNLOAD - abort a load when browser is loading or loaded
    def unload(self):
        self.mon.trace(self, '')
        self.mon.log(self,
                     ">unload received from show Id: " + str(self.show_id))
        self.driver_close()
        self.play_state = 'closed'

    # SHOW - show a track from its loaded state
    def show(self, ready_callback, finished_callback, closed_callback):

        # instantiate arguments
        self.ready_callback = ready_callback  # callback when ready to show a web page-
        self.finished_callback = finished_callback  # callback when finished showing  - not used
        self.closed_callback = closed_callback  # callback when closed

        self.mon.trace(self, '')

        self.play_state = 'showing'
        # init state and signals
        self.quit_signal = False
        # do common bits
        Player.pre_show(self)
        #self.driver.get(self.current_url)
        self.duration_count = self.duration
        self.tick_timer = self.canvas.after(10, self.show_state_machine)

    def show_state_machine(self):

        if self.play_state == 'showing':
            self.duration_count -= 1
            # self.mon.log(self,"      Show state machine: " + self.show_state)

            # service any queued stop signals and test duration count
            if self.quit_signal is True or (self.duration != 0
                                            and self.duration_count == 0):
                self.mon.log(self,
                             "      Service stop required signal or timeout")
                if self.command_timer != None:
                    self.canvas.after_cancel(self.command_timer)
                    self.command_timer = None
                if self.quit_signal is True:
                    self.quit_signal = False
                if self.freeze_at_end == 'yes':
                    self.mon.log(self, 'chrome says pause_at_end')
                    if self.finished_callback is not None:
                        self.finished_callback('pause_at_end', 'pause at end')
                        self.tick_timer = self.canvas.after(
                            50, self.show_state_machine)
                else:
                    self.mon.log(self, 'chrome says niceday')
                    self.driver_close()
                    self.play_state = 'closed'
                    if self.closed_callback is not None:
                        self.closed_callback('normal', 'chromedriver closed')
                    return
            else:
                self.tick_timer = self.canvas.after(50,
                                                    self.show_state_machine)

    # CLOSE - nothing to do in browserplayer - x content is removed by ready callback and hide browser does not implement pause_at_end
    def close(self, closed_callback):
        self.mon.trace(self, '')
        self.closed_callback = closed_callback
        self.mon.log(self,
                     ">close received from show Id: " + str(self.show_id))
        self.driver_close()
        self.play_state = 'closed'
        # PP does not use close callback but it does read self.play_state

    def input_pressed(self, symbol):
        self.mon.trace(self, symbol)
        if symbol == 'pause':
            self.pause()
        elif symbol == 'pause-on':
            self.pause_on()
        elif symbol == 'pause-off':
            self.pause_off()
        elif symbol == 'stop':
            self.stop()

    # browsers do not do pause
    def pause(self):
        self.mon.log(self, "!<pause rejected")
        return False

    # browsers do not do pause
    def pause_on(self):
        self.mon.log(self, "!<pause on rejected")
        return False

    # browsers do not do pause
    def pause_off(self):
        self.mon.log(self, "!<pause off rejected")
        return False

    # respond to normal stop
    def stop(self):
        # send signal to stop the track to the state machine
        self.mon.log(self, ">stop received")
        self.quit_signal = True

# ***********************
# veneer for controlling chromium browser
# ***********************

    def driver_open(self):
        tries = 4
        while tries > 0:
            try:
                self.driver = webdriver.Chrome(options=self.chrome_options)
                return
            except Exception as e:
                #print ("Failed to open Chromium", e, e.__class__,tries)
                tries -= 1

    def driver_close(self):
        try:
            self.driver.quit()
        except WebDriverException as e:
            self.mon.warn(self, 'Browser Closed in Close !!!!!!\n' + str(e))
        except Exception as e:
            print("Oops!", e, e.__class__, "occurred.")
        else:
            return

    def driver_refresh(self):
        try:
            self.driver.refresh()
        except WebDriverException as e:
            self.mon.warn(self, 'Browser Closed in Refresh !!!!!!\n' + str(e))
        except Exception as e:
            print("Oops!", e, e.__class__, "occurred.")
        else:
            return

    def driver_get(self, url):
        self.mon.log(self, 'get: ' + url)
        try:
            self.driver.get(url)
        except WebDriverException as e:
            self.mon.warn(self, 'Browser Closed in Get !!!!!!\n' + str(e))
        except Exception as e:
            print("Oops!", e, e.__class__, "occurred.")
        else:
            return

# *******************
# browser commands
# ***********************

    def parse_commands(self, command_text):
        self.command_list = []
        self.max_loops = -1  #loop continuous if no loop command
        lines = command_text.split('\n')
        for line in lines:
            if line.strip() == '':
                continue
            #print (line)
            reason, entry = self.parse_command(line)
            if reason != 'normal':
                return 'error', entry
            self.command_list.append(copy.deepcopy(entry))

        num_loops = 0
        for entry in self.command_list:
            if entry[0] == 'loop':
                num_loops += 1
            if num_loops > 1:
                return 'error', str(
                    num_loops) + ' loop commands in browser commands'
        return 'normal', ''

    def parse_command(self, line):
        fields = line.split()
        #print (fields)
        if len(fields) not in (1, 2):
            return 'error', "incorrect number of fields in command: " + line
        command = fields[0]
        arg = ''

        if command not in ('load', 'refresh', 'wait', 'loop'):
            return 'error', 'unknown browser command: ' + line

        if command in ('refresh', ) and len(fields) != 1:
            return 'error', 'incorrect number of fields for ' + command + 'in: ' + line

        if command in ('refresh', ):
            return 'normal', [command, '']

        if command == 'load':
            if len(fields) != 2:
                return 'error', 'incorrect number of fields for ' + command + 'in: ' + line

            arg = fields[1]
            track = self.complete_path(arg)
            # does media exist
            if not ':' in track:
                if not os.path.exists(track):
                    return 'error', 'cannot find file: ' + track

            # add file:// to files.
            if ':' in track:
                url = track
            else:
                url = 'file://' + track

            return 'normal', [command, url]

        if command == 'loop':
            if len(fields) == 1:
                arg = '-1'
                self.max_loops = -1  #loop continuously if no argument
                return 'normal', [command, arg]

            elif len(fields) == 2:
                if not fields[1].isdigit() or fields[1] == '0':
                    return 'error', 'Argument for Loop is not a positive number in: ' + line
                else:
                    arg = fields[1]
                    self.max_loops = int(arg)
                return 'normal', [command, arg]

            else:
                return 'error', 'incorrect number of fields for ' + command + 'in: ' + line

        if command == 'wait':
            if len(fields) != 2:
                return 'error', 'incorrect number of fields for ' + command + 'in: ' + line
            else:
                arg = fields[1]
                if not arg.isdigit():
                    return 'error', 'Argument for Wait is not 0 or positive number in: ' + line
                else:
                    return 'normal', [command, arg]

    def play_commands(self):
        # init
        if len(self.command_list) == 0:
            return
        self.loop_index = -1  # -1 no loop  comand found
        self.loop_count = 0
        self.command_index = 0
        self.next_command_index = 0  #start at beginning
        #loop round executing the commands
        self.canvas.after(100, self.execute_command)

    def execute_command(self):
        self.command_index = self.next_command_index
        if self.command_index == len(self.command_list):
            # past end of command list
            self.quit_signal = True
            return

        if self.command_index == len(
                self.command_list) - 1 and self.loop_index != -1:
            # last in list and need to loop
            self.next_command_index = self.loop_index
        else:
            self.next_command_index = self.command_index + 1

        entry = self.command_list[self.command_index]
        command = entry[0]
        arg = entry[1]
        self.mon.log(
            self,
            str(self.command_index) + ' Do ' + command + ' ' + arg +
            '  Next: ' + str(self.next_command_index))

        # and execute command
        if command == 'load':
            self.driver_get(arg)
            self.command_timer = self.canvas.after(10, self.execute_command)

        elif command == 'refresh':
            self.driver_refresh()
            self.command_timer = self.canvas.after(10, self.execute_command)

        elif command == 'wait':
            self.command_timer = self.canvas.after(1000 * int(arg),
                                                   self.execute_command)

        elif command == 'loop':
            if self.loop_index == -1:
                # found loop for first time
                self.loop_index = self.command_index
                self.loop_count = 0
                self.mon.log(
                    self, 'Loop init To: ' + str(self.loop_index) +
                    '  Count: ' + str(self.loop_count))
                self.command_timer = self.canvas.after(10,
                                                       self.execute_command)
            else:
                self.loop_count += 1
                self.mon.log(
                    self,
                    'Inc loop count: ' + '  Count: ' + str(self.loop_count))
                # hit loop command after the requied number of loops
                if self.loop_count == self.max_loops:  #max loops is -1 for continuous
                    self.mon.log(
                        self,
                        'end of loop: ' + '  Count: ' + str(self.loop_count))
                    self.quit_signal = True
                    return
                else:
                    self.mon.log(
                        self, 'Looping to: ' + str(self.loop_index) +
                        ' Count: ' + str(self.loop_count))
                    self.command_timer = self.canvas.after(
                        10, self.execute_command)

        elif command == 'exit':
            self.quit_signal = True
            return

    def process_chrome_options(self):
        self.chrome_options = Options()
        #self.add_option("--incognito")
        self.add_option("--noerrdialogs")
        self.add_option("--disable-infobars")
        self.add_option("--check-for-update-interval=31536000")
        self.add_option('--disable-overlay-scrollbar')
        self.chrome_options.add_experimental_option("excludeSwitches",
                                                    ['enable-automation'])

        try:
            self.zoom = float(self.chrome_zoom_text)
        except ValueError:
            return 'error', 'Chrome Zoom is not a number' + self.chrome_zoom_text

        self.add_option('--force-device-scale-factor=' + self.chrome_zoom_text)

        status, message = self.process_chrome_window(self.chrome_window_text)
        if status == 'error':
            return 'error', message

        status, message = self.add_other_options()
        if status == 'error':
            return 'error', message

        return 'normal', ''

    def add_other_options(self):
        opts_list = self.chrome_other_options.split(' ')
        #print (opts_list)
        for opt in opts_list:
            if opt == '':
                continue
            if opt[0:2] != '--':
                return 'error', 'option is not preceded by -- :  ' + opt
            else:
                self.add_option(opt)
        return 'normal', ''

    def add_option(self, option):
        #print ('Adding Option: ',option)
        self.chrome_options.add_argument(option)

    def process_chrome_window(self, line):
        #parse chrome window
        # kiosk,fullscreen,showcanvas,display
        # obxprop | grep "^_OB_APP"  and click the window

        self.app_mode = False

        # showcanvas|display +  [x+y+w*h]
        words = line.split()
        if len(words) not in (1, 2):
            return 'error', 'bad Chrome Web Window form ' + line

        if words[0] not in ('display', 'showcanvas', 'kiosk', 'fullscreen'):
            return 'error', 'No or invalid Chrome Web Window mode: ' + line

        if len(words) == 1 and words[0] == 'kiosk':
            self.add_option('--kiosk')
            x_org, y_org = self.dm.real_display_position(self.display_id)
            self.add_option('--window-position=' + str(x_org) + ',' +
                            str(y_org))
            return 'normal', ''

        if len(words) == 1 and words[0] == 'fullscreen':
            self.add_option('--start-fullscreen')
            x_org, y_org = self.dm.real_display_position(self.display_id)
            self.add_option('--window-position=' + str(x_org) + ',' +
                            str(y_org))
            return 'normal', ''

        # display or showcanvas with or without dimensions
        self.app_mode = True
        if words[0] == 'display':
            x_org, y_org = self.dm.real_display_position(self.display_id)
            width, height = self.dm.real_display_dimensions(self.display_id)

        if words[0] == 'showcanvas':
            x_org, y_org = self.dm.real_display_position(self.display_id)
            x_org += self.show_canvas_x1
            y_org += self.show_canvas_y1
            width = self.show_canvas_width
            height = self.show_canvas_height

        x_offset = 0
        y_offset = 0
        #calc offset and width/height from dimensions
        if len(words) > 1:
            status, message, x_offset, y_offset, width, height = self.parse_dimensions(
                words[1], width, height)
            if status == 'error':
                return 'error', message

        #correct for zoom
        width = int(width / self.zoom)
        height = int(height / self.zoom)

        x = x_org + x_offset
        y = y_org + y_offset
        self.chrome_window_x = x
        self.chrome_window_y = y
        self.chrome_window_width = width
        self.chrome_window_height = height
        #print ('app',self.app_mode,x,y,width,height)
        self.add_option('--app=' + self.current_url)
        self.add_option('--window-size=' + str(width) + ',' + str(height))
        self.add_option('--window-position=' + str(x) + ',' + str(y))
        return 'normal', ''

    def parse_dimensions(self, dim_text, show_width, show_height):
        if '+' in dim_text:
            # parse x+y+width*height
            fields = dim_text.split('+')
            if len(fields) != 3:
                return 'error', 'bad chrome window form ' + dim_text, 0, 0, 0, 0

            if not fields[0].isdigit():
                return 'error', 'x is not a positive decimal in chrome web window ' + dim_text, 0, 0, 0, 0
            else:
                x = int(fields[0])

            if not fields[1].isdigit():
                return 'error', 'y is not a positive decimal in chrome webwindow ' + dim_text, 0, 0, 0, 0
            else:
                y = int(fields[1])

            dimensions = fields[2].split('*')
            if len(dimensions) != 2:
                return 'error', 'bad chrome web window dimensions ' + dim_text, '', 0, 0, 0, 0

            if not dimensions[0].isdigit():
                return 'error', 'width is not a positive decimal in chrome web window ' + dim_text, 0, 0, 0, 0
            else:
                width = int(dimensions[0])

            if not dimensions[1].isdigit():
                return 'error', 'height is not a positive decimal in chrome web window ' + dim_text, 0, 0, 0, 0
            else:
                height = int(dimensions[1])

            return 'normal', '', x, y, width, height
        else:
            #width*height
            dimensions = dim_text.split('*')
            if len(dimensions) != 2:
                return 'error', 'bad chrome web window dimensions ' + line, '', 0, 0, 0, 0

            if not dimensions[0].isdigit():
                return 'error', 'width is not a positive decimal in chrome web window ' + line, '', 0, 0, 0, 0
            else:
                window_width = int(dimensions[0])

            if not dimensions[1].isdigit():
                return 'error', 'height is not a positive decimal in chrome web window ' + line, '', 0, 0, 0, 0
            else:
                window_height = int(dimensions[1])

            x = int((show_width - window_width) / 2)
            y = int((show_height - window_height) / 2)
            return 'normal', '', x, y, window_width, window_height