#!/usr/bin/python3 # $HeadURL: https://svn.pasta.freemyip.com/main/ade/tags/3.0.3/share/templates/xxx/bin/xxx $ $LastChangedRevision: 11136 $ # The script is a replacement for xscreensaver, which suffers from two bugs: # # 1) *sometimes, randomly*, the screen goes dark, but not actually off. # This may be #1006199 but the second message suggests the issue was fixed # in 6.02, whereas I'm running 6.06, so either not fixed or not right bug. # # 2) the "Quick Power-off in Blank Only Mode" setting is not persistent (run # xscreensaver-settings, enable that setting, exit, rerun, setting is # disabled again). This is BTS#1031076 and BTS#1055835. # # TODO (in this order): # # 0) Add locking. # 1) The two arrays desired_xset_dpms_settings and desired_xset_s_settings # should be replaced with one dictionary. Several bits in the code will # need changing from using indexes or slices to key or key lists. # # 2) Add checks that xscreensaver isn't running. # # 3) The check that the nudge check interval is more frequent that the # DPMS/xset-s timeout doesn't factor in that one of those is disabled. # E.g. if DPMS is enabled with timeout 30, and xset-s is disabled # with timeout 15 then it's that 15 that will be compared with the # nudge check interval. # # 4) is the browser running a video? # Modules import subprocess import os import sys sys.path.append(subprocess.Popen(['miniade', '--dirname'], stdout=subprocess.PIPE, universal_newlines=True).communicate()[0].rstrip()) import miniade import re import time # Configurable stuff # It will be interesting to see if the DPMS timeout is adequate for the *last # few minutes* of a film, when the film file has been read and so its read() # offset is not increasing so the check_nudge() function thinks the movie # is paused and so *doesn't* nudge the screensaver! If that happens then # I probably need to increase the DPMS timeout or write some movie-looks-paused- # but-lets-nudge-screensaver-anyway code. desired_xset_dpms_settings = [ True, # DMPS enabled? 0, # standby timeout 0, # suspend timeout 180 # off timeout ] # We rely on the DPMS settings, not the "screensaver" settings. ("screensaver" is # a bit confusing; this is not xscreensaver (or whatever) but rather something # that either xset itself implements or that allows xscreensaver to register # callbacks with.) desired_xset_s_settings = [ False, # blank enabled? False, # expose enabled? 0, # screensaver timeout 0 # screensaver cycle timeout ] check_intervals = { 'nudge':60, # how often we check if a nudge is needed? 'xserver':120, # how often we check if xserver PID still running? 'dpms':60 # how often we check if DPMS or 's' settings have been tampered with? } # Other globals def main(): global re_movie_filename, re_player_filename, check_intervals, previous_running_movie_offsets, xserver_pid, last_nudge_timestamp, previous_mouse_position # Defaults for options re_movie_filename = '^(?:/dev/sr\d|.*\.(?:avi|bup|flv|m4v|mkv|mp4|mpg|ogv|vob|wmv|mov))$' re_player_filename = '^.*$' # Process options def special_opts_handler(): global re_movie_filename, re_player_filename if sys.argv[1] == '-m': re_movie_filename = sys.argv[2] elif sys.argv[1].startswith('--re-movie='): re_movie_filename = sys.argv[1][len('--re-movie='):] elif sys.argv[1] == '-p': re_player_filename = sys.argv[2] elif sys.argv[1].startswith('--re-player='): re_player_filename = sys.argv[1][len('--re-player='):] else: return False return True miniade.process_options(special_opts_handler, help) # Process arguments if len(sys.argv) != 1: miniade.bad_usage() # Sanity checks and derivations # Check option conflicts. if desired_xset_dpms_settings[0] == desired_xset_s_settings[0]: miniade.error('built-in options specify using both/neither of DPMS and "screensaver" to manage screen (hint: use only one of them)') # Make sure screensaver won't kick in before nudge check has a chance to postpone screensaver. if min([ x for x in desired_xset_dpms_settings[1:]+desired_xset_s_settings[2:] if x != 0 ]) <= check_intervals['nudge']: miniade.error('nudge check interval is less than twice screensaver timeout (hint: increase screensaver timeout or decrease nudge check interval)') # Instantiate empty arrays and dicts. checks_due_timestamps = {} if os.getuid == 0: miniade.error('don\'t run this as root') # Get the PID of the X server (this must be done before locking since the lock file name is based on that PID) xserver_pid = get_xserver_pid() # Check we can talk to X server miniade.debug(10,'main: checking we can talk to X server ...') if my_system('xlsfonts')[0] != 0: miniade.error('can\'t talk to X server') # In how many seconds from now should we run each check. Initially, all checks need # to be done, so all timestamps are 0 to force assessment that due. checks_due_timestamps = { 'xserver':0, 'nudge':0, 'dpms':0, } # We could not define handlers in globals above since their values were not yet # defined. handlers = { 'xserver': check_xserver, 'nudge': check_nudge, 'dpms': check_dpms } last_nudge_timestamp = time.time() # To know if a movie is running (and not paused) we record the offsets of all open movie # files and compare them with how they were previously. Here we initialise those previous # values to comparison may proceed. previous_running_movie_offsets = {} previous_mouse_position = (-1,-1) current_timestamp = 0 # Guts miniade.debug(10, 'main: entering monitoring loop ...') while True: # Work out which check is due next and when and sleep until then. soonest_check = min(checks_due_timestamps, key=checks_due_timestamps.get) sleep_period = max([0, checks_due_timestamps[soonest_check] - current_timestamp]) miniade.debug(10, 'main: will check %s in %.2fs ...' % (soonest_check, sleep_period)) time.sleep(sleep_period) # Even before calling the check function, work out what the new check time # for the check. We do this to try to minimise the drift in start time caused # by the check itself taking some time. Beware that the *top* of this loop # consults current_timestamp, so don't be tempted to remove this assignment. current_timestamp = time.time() checks_due_timestamps[soonest_check] = time.time() + check_intervals[soonest_check] # Do the check, and break out of loop if check function told us to. if not handlers[soonest_check](): break return 0 def help(): progname = miniade.get_progname() print('Usage: %s [ ] [ -m | --re-movie= ] [ -p | --re-player= ]\n' % (progname)) sys.exit(0) def check_nudge(): global previous_running_movie_offsets, desired_xset_dpms_settings, last_nudge_timestamp, previous_mouse_position # One-loop only while True: # Get position in all open movies and work out if nudge needed. running_movie_offsets = get_running_movie_offsets() miniade.debug(10, 'check_nudge: running_movie_offsets is %s' % (running_movie_offsets)) if running_movie_offsets != previous_running_movie_offsets: miniade.debug(10, 'check_nudge: movie offsets have changed') previous_running_movie_offsets = running_movie_offsets nudge_needed = True break mouse_position = get_mouse_position() miniade.debug(10, 'check_nudge: mouse_position is %s' % (repr(mouse_position))) if mouse_position != previous_mouse_position: miniade.debug(10, 'check_nudge: mouse position has changed') previous_mouse_position = mouse_position nudge_needed = True break # Has mouse moved? # This break ensures loop is one loop only. nudge_needed = False break miniade.debug(10, 'check_nudge: %snudging ...' % ('' if nudge_needed else 'not ')) if nudge_needed: if my_system('xset dpms force on')[0] != 0: miniade.error('xset dpms force on: failed') last_nudge_timestamp = time.time() miniade.debug(10, 'check_nudge: screen will blank in %.2fs' % (desired_xset_dpms_settings[3] - (time.time()-last_nudge_timestamp))) return True def check_xserver(): global xserver_pid if not os.path.isdir('/proc/' + str(xserver_pid)): return False return True def check_dpms(): global desired_xset_dpms_settings, desired_xset_s_settings # Run xset rc, output = my_system('xset q') if rc != 0: miniade.error('xset q: failed') # Extract DPMS settings. m = re.search('(?:.*\n|^)DPMS \(Energy Star\):\n *Standby: *([0-9]+) *Suspend: *([0-9]+) *Off: ([0-9]+)\n *DPMS is (Enabled|Disabled)\n.*', output, flags=re.DOTALL) if not m: miniade.error('xset q: failed to extract DMPS settings') actual_xset_dpms_settings = [ {'Enabled':True,'Disabled':False}[m.group(4)], int(m.group(1)), int(m.group(2)), int(m.group(3)) ] miniade.debug(10, 'check_dpms: desired_xset_dpms_settings=%s, actual_xset_dpms_settings=%s' % (desired_xset_dpms_settings, actual_xset_dpms_settings)) if desired_xset_dpms_settings != actual_xset_dpms_settings: miniade.warning('DPMS settings have changed behind our back! fixing ...') my_system('xset dpms %d %d %d' % (tuple(desired_xset_dpms_settings[1:]))) my_system('xset %sdpms' % {True:'+', False:'-'}[desired_xset_dpms_settings[0]]) # Extract "screensaver" settings. m = re.search('(?:.*\n|^)Screen Saver:\n *prefer blanking: *(no|yes) *allow exposures: *(no|yes)\n *timeout: *([0-9]+) *cycle: *([0-9]+)\n.*', output, flags=re.DOTALL) if not m: miniade.error('xset q: failed to extract "screensaver" settings') actual_xset_s_settings = [ {'yes':True,'no':False}[m.group(1)], {'yes':True,'no':False}[m.group(2)], int(m.group(3)), int(m.group(4)) ] miniade.debug(10, 'check_dpms: desired_xset_s_settings=%s, actual_xset_s_settings=%s' % (desired_xset_s_settings, actual_xset_s_settings)) if desired_xset_s_settings != actual_xset_s_settings: miniade.warning('"screensaver" settings have changed behind our back! fixing ...') my_system('xset s %sblank %sexpose' % ({True:'',False:'no'}[desired_xset_s_settings[0]], {True:'',False:'no'}[desired_xset_s_settings[1]])) my_system('xset s %d %d' % (tuple(desired_xset_s_settings[2:]))) return True def my_system(cmd): try: child = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) except FileNotFoundError: return 1, None output = child.communicate()[0] rc = child.returncode miniade.debug(10, 'my_system: rc=%d, output=[%s]' % (rc, output[:10]+'...')) return rc, output def get_xserver_pid(): # Check DISPLAY set and sensible. if not 'DISPLAY' in os.environ: miniade.error('DISPLAY: not set') if len(os.environ['DISPLAY']) == 0: miniade.error('DISPLAY: set but empty') restuff = re.search('^:(\d+)(?:\.\d+)?$', os.environ['DISPLAY']) if restuff is None: miniade.error('DISPLAY: failed to parse') # Read X server pid from lock file miniade.debug(10, 'get_xserver_pid: display no = %s' % (restuff.group(1))) x_lock_file = '/tmp/.X' + restuff.group(1) + '-lock' try: fh = open(x_lock_file, 'r') except IOError: miniade.error('%s: can\'t open' % (x_lock_file)) pid = int(fh.readline()[:-1].lstrip(' ')) fh.close() miniade.debug(10, 'get_xserver_pid: pid=%d' % (pid)) # Check lock file not stale. if not os.path.isdir(os.path.join('/proc', str(pid))): miniade.error('stale X lock file?') # Return pid to caller. return pid def get_running_movie_offsets(): global re_player_filename, re_movie_filename running_movie_offsets = { } # For each running process ... for pid in os.listdir('/proc'): # ... filter and canonicalise ... if re.search('^\d+$', pid) is None: continue pathname = os.path.join('/proc', pid) try: pathname_stat = os.stat(pathname) except OSError: continue if pathname_stat.st_uid != os.getuid(): continue # Skip if not directory. (stat module has convenience functions; os.stat # seemingly does not.) if not oct(pathname_stat.st_mode) and 0o040000: continue # Compare player regexps with target of /proc/$PID/exe symlink. try: exe = os.readlink(os.path.join(pathname, 'exe')) except OSError: continue if re.search(re_player_filename, exe) is None: continue # ... look at what files it has open ... try: fds = os.listdir(os.path.join(pathname, 'fd')) except OSError: fds = [] for fd in fds: try: openfilename = os.readlink(os.path.join(pathname, 'fd', fd)) except OSError: continue # ... ignore non-movies ... if re.search(re_movie_filename, openfilename) is None: continue # ... get how much read from movie file ... try: fh = open(os.path.join(pathname,'fdinfo',fd)) except OSError: continue # ... and record that information. running_movie_offsets[openfilename] = int(fh.readline()[5:][:-1]) fh.close() # Pass back to caller a dictionary of movie-files/offsets. return running_movie_offsets def get_mouse_position(): rc, output = my_system('xdotool getmouselocation') if rc != 0: miniade.error('xdotool getmouselocation: failed') m = re.search('^x:(\d+) y:(\d+) screen:(\d+) window:(\d+)$', output) if not m: miniade.error('xdotool getmouselocation: failed to extract mouse position') return (int(m.group(1)), int(m.group(2))) # Entry point if __name__=="__main__": main()