]> git.donarmstrong.com Git - deb_pkgs/autorandr.git/blobdiff - autorandr.py
Call autorandr via systemd from udev, remove pmutils script on systemd systems
[deb_pkgs/autorandr.git] / autorandr.py
index 07febc3f65370aab504c0519b5a63faabcd8b882..ad4b22b0fbeec18e7e89695d4c9a481f76cc74a9 100755 (executable)
@@ -72,7 +72,7 @@ Usage: autorandr [options]
 --debug                 enable verbose output
 --batch                 run autorandr for all users with active X11 sessions
 
- To prevent a profile from being loaded, place a script call "block" in its
+ To prevent a profile from being loaded, place a script called "block" in its
  directory. The script is evaluated before the screen setup is inspected, and
  in case of it returning a value of 0 the profile is skipped. This can be used
  to query the status of a docking station you are about to leave.
@@ -99,18 +99,25 @@ class AutorandrException(Exception):
             while trace.tb_next:
                 trace = trace.tb_next
             self.line = trace.tb_lineno
+            self.file_name = trace.tb_frame.f_code.co_filename
         else:
             try:
                 import inspect
-                self.line = inspect.currentframe().f_back.f_lineno
+                frame = inspect.currentframe().f_back
+                self.line = frame.f_lineno
+                self.file_name = frame.f_code.co_filename
             except:
                 self.line = None
+                self.file_name = None
             self.original_exception = None
 
+        if os.path.abspath(self.file_name) == os.path.abspath(sys.argv[0]):
+            self.file_name = None
+
     def __str__(self):
         retval = [ self.message ]
         if self.line:
-            retval.append(" (line %d)" % self.line)
+            retval.append(" (line %d%s)" % (self.line, ("; %s" % self.file_name) if self.file_name else ""))
         if self.original_exception:
             retval.append(":\n  ")
             retval.append(str(self.original_exception).replace("\n", "\n  "))
@@ -338,6 +345,8 @@ class XrandrOutput(object):
         for line in configuration.split("\n"):
             if line:
                 line = line.split(None, 1)
+                if line and line[0].startswith("#"):
+                    continue
                 options[line[0]] = line[1] if len(line) > 1 else None
 
         edid = None
@@ -457,7 +466,7 @@ def load_profiles(profile_path):
         if not os.path.isfile(config_name) or not os.path.isfile(setup_name):
             continue
 
-        edids = dict([ x.strip().split() for x in open(setup_name).readlines() if x.strip() ])
+        edids = dict([ x.split() for x in (y.strip() for y in open(setup_name).readlines()) if x and x[0] != "#" ])
 
         config = {}
         buffer = []
@@ -550,12 +559,18 @@ def call_and_retry(*args, **kwargs):
     waits a second and then retries once. This mitigates #47,
     a timing issue with some drivers.
     """
-    kwargs_redirected = dict(kwargs)
-    if hasattr(subprocess, "DEVNULL"):
-        kwargs_redirected["stdout"] = getattr(subprocess, "DEVNULL")
+    if "dry_run" in kwargs:
+        dry_run = kwargs["dry_run"]
+        del kwargs["dry_run"]
     else:
-        kwargs_redirected["stdout"] = open(os.devnull, "w")
-    kwargs_redirected["stderr"] = kwargs_redirected["stdout"]
+        dry_run = False
+    kwargs_redirected = dict(kwargs)
+    if not dry_run:
+        if hasattr(subprocess, "DEVNULL"):
+            kwargs_redirected["stdout"] = getattr(subprocess, "DEVNULL")
+        else:
+            kwargs_redirected["stdout"] = open(os.devnull, "w")
+        kwargs_redirected["stderr"] = kwargs_redirected["stdout"]
     retval = subprocess.call(*args, **kwargs_redirected)
     if retval != 0:
         time.sleep(1)
@@ -615,13 +630,13 @@ def apply_configuration(new_configuration, current_configuration, dry_run=False)
     # Perform pe-change auxiliary changes
     if auxiliary_changes_pre:
         argv = base_argv + list(chain.from_iterable(auxiliary_changes_pre))
-        if call_and_retry(argv) != 0:
+        if call_and_retry(argv, dry_run=dry_run) != 0:
             raise AutorandrException("Command failed: %s" % " ".join(argv))
 
     # Disable unused outputs, but make sure that there always is at least one active screen
     disable_keep = 0 if remain_active_count else 1
     if len(disable_outputs) > disable_keep:
-        if call_and_retry(base_argv + list(chain.from_iterable(disable_outputs[:-1] if disable_keep else disable_outputs))) != 0:
+        if call_and_retry(base_argv + list(chain.from_iterable(disable_outputs[:-1] if disable_keep else disable_outputs)), dry_run=dry_run) != 0:
             # Disabling the outputs failed. Retry with the next command:
             # Sometimes disabling of outputs fails due to an invalid RRSetScreenSize.
             # This does not occur if simultaneously the primary screen is reset.
@@ -640,7 +655,7 @@ def apply_configuration(new_configuration, current_configuration, dry_run=False)
     operations = disable_outputs + enable_outputs
     for index in range(0, len(operations), 2):
         argv = base_argv + list(chain.from_iterable(operations[index:index+2]))
-        if call_and_retry(argv) != 0:
+        if call_and_retry(argv, dry_run=dry_run) != 0:
             raise AutorandrException("Command failed: %s" % " ".join(argv))
 
 def is_equal_configuration(source_configuration, target_configuration):
@@ -729,6 +744,8 @@ def exec_scripts(profile_path, script_name, meta_information=None):
     and system-wide configuration folders, named script_name or residing in
     subdirectories named script_name.d.
 
+    If profile_path is None, only global scripts will be invoked.
+
     meta_information is expected to be an dictionary. It will be passed to the block scripts
     in the environment, as variables called AUTORANDR_<CAPITALIZED_KEY_HERE>.
 
@@ -748,13 +765,19 @@ def exec_scripts(profile_path, script_name, meta_information=None):
     if not os.path.isdir(user_profile_path):
         user_profile_path = os.path.join(os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), "autorandr")
 
-    for folder in chain((profile_path, os.path.dirname(profile_path), user_profile_path),
-                        (os.path.join(x, "autorandr") for x in os.environ.get("XDG_CONFIG_DIRS", "/etc/xdg").split(":"))):
+    candidate_directories = chain((user_profile_path,), (os.path.join(x, "autorandr") for x in os.environ.get("XDG_CONFIG_DIRS", "/etc/xdg").split(":")))
+    if profile_path:
+        candidate_directories = chain((profile_path,), candidate_directories)
+
+    for folder in candidate_directories:
 
         if script_name not in ran_scripts:
             script = os.path.join(folder, script_name)
             if os.access(script, os.X_OK | os.F_OK):
-                all_ok &= subprocess.call(script, env=env) != 0
+                try:
+                    all_ok &= subprocess.call(script, env=env) != 0
+                except:
+                    raise AutorandrException("Failed to execute user command: %s" % (script,))
                 ran_scripts.add(script_name)
 
         script_folder = os.path.join(folder, "%s.d" % script_name)
@@ -764,7 +787,10 @@ def exec_scripts(profile_path, script_name, meta_information=None):
                 if check_name not in ran_scripts:
                     script = os.path.join(script_folder, file_name)
                     if os.access(script, os.X_OK | os.F_OK):
-                        all_ok &= subprocess.call(script, env=env) != 0
+                        try:
+                            all_ok &= subprocess.call(script, env=env) != 0
+                        except:
+                            raise AutorandrException("Failed to execute user command: %s" % (script,))
                         ran_scripts.add(check_name)
 
     return all_ok
@@ -798,7 +824,14 @@ def dispatch_call_to_sessions(argv):
         if not os.path.isfile(environ_file):
             continue
         uid = os.stat(environ_file).st_uid
-        if uid == 0:
+
+        # The following line assumes that user accounts start at 1000 and that
+        # no one works using the root or another system account. This is rather
+        # restrictive, but de facto default. Alternatives would be to use the
+        # UID_MIN from /etc/login.defs or FIRST_UID from /etc/adduser.conf;
+        # but effectively, both values aren't binding in any way.
+        # If this breaks your use case, please file a bug on Github.
+        if uid < 1000:
             continue
 
         process_environ = {}
@@ -810,6 +843,9 @@ def dispatch_call_to_sessions(argv):
                 process_environ[name] = value
         display = process_environ["DISPLAY"] if "DISPLAY" in process_environ else None
 
+        # To allow scripts to detect batch invocation (especially useful for predetect)
+        process_environ["AUTORANDR_BATCH_PID"] = os.getpid()
+
         if display and display not in X11_displays_done:
             try:
                 pwent = pwd.getpwuid(uid)
@@ -845,6 +881,9 @@ def main(argv):
               file=sys.stderr)
         sys.exit(posix.EX_USAGE)
 
+    if "-h" in options or "--help" in options:
+        exit_help()
+
     # Batch mode
     if "--batch" in options:
         if ("DISPLAY" not in os.environ or not os.environ["DISPLAY"]) and os.getuid() == 0:
@@ -879,6 +918,7 @@ def main(argv):
 
     profile_symlinks = { k: v for k, v in profile_symlinks.items() if v in (x[0] for x in virtual_profiles) or v in profiles }
 
+    exec_scripts(None, "predetect")
     config, modes = parse_xrandr_output()
 
     if "--fingerprint" in options:
@@ -938,9 +978,6 @@ def main(argv):
             raise AutorandrException("Failed to remove profile '%s'" % (options["--remove"],), e)
         sys.exit(0)
 
-    if "-h" in options or "--help" in options:
-        exit_help()
-
     detected_profiles = find_profiles(config, profiles)
     load_profile = False
 
@@ -1022,6 +1059,8 @@ def main(argv):
                     apply_configuration(load_config, config, True)
                 apply_configuration(load_config, config, False)
                 exec_scripts(scripts_path, "postswitch", script_metadata)
+        except AutorandrException as e:
+            raise AutorandrException("Failed to apply profile '%s'" % load_profile, e, e.report_bug)
         except Exception as e:
             raise AutorandrException("Failed to apply profile '%s'" % load_profile, e, True)
 
@@ -1033,7 +1072,7 @@ def main(argv):
 
     sys.exit(0)
 
-if __name__ == '__main__':
+def exception_handled_main(argv=sys.argv):
     try:
         main(sys.argv)
     except AutorandrException as e:
@@ -1046,3 +1085,6 @@ if __name__ == '__main__':
 
         print("Unhandled exception ({0}). Please report this as a bug at https://github.com/phillipberndt/autorandr/issues.".format(e), file=sys.stderr)
         raise
+
+if __name__ == '__main__':
+    exception_handled_main()