]> git.donarmstrong.com Git - deb_pkgs/autorandr.git/commitdiff
Introduce --batch option to autorandr: Run autorandr for each user with an X11 session
authorPhillip Berndt <phillip.berndt@googlemail.com>
Fri, 16 Sep 2016 14:19:24 +0000 (16:19 +0200)
committerPhillip Berndt <phillip.berndt@googlemail.com>
Fri, 16 Sep 2016 14:19:24 +0000 (16:19 +0200)
This is an attempt to resolve #45 and it might also be a better
alternative to #52, #44 and #39.

Makefile
autorandr.py
contrib/pm-utils/40autorandr
contrib/systemd/autorandr-resume.service
contrib/udev/40-monitor-hotplug.rules

index a312da8fde60c30c0d4ef038af2e7900407d8591..10570e48618fdd98b8166c5ffc329751c72446a1 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -50,10 +50,10 @@ ifeq ($(HAVE_SYSTEMD),y)
 DEFAULT_TARGETS+=systemd
 endif
 
-install_systemd: install_pmutils
+install_systemd:
        install -D -m 644 contrib/systemd/autorandr-resume.service ${DESTDIR}/etc/systemd/system/autorandr-resume.service
 
-uninstall_systemd: uninstall_pmutils
+uninstall_systemd:
        rm -f ${DESTDIR}/etc/systemd/system/autorandr-resume.service
 
 # Rules for udev
@@ -62,7 +62,7 @@ ifeq ($(HAVE_UDEV),y)
 DEFAULT_TARGETS+=udev
 endif
 
-install_udev: install_pmutils
+install_udev:
        install -D -m 644 contrib/udev/40-monitor-hotplug.rules ${DESTDIR}/etc/udev/rules.d/40-monitor-hotplug.rules
 ifeq (${USER},root)
        udevadm control --reload-rules
@@ -71,7 +71,7 @@ else
        @echo "    udevadm control --reload-rules"
 endif
 
-uninstall_udev: uninstall_pmutils
+uninstall_udev:
        rm -f ${DESTDIR}/etc/udev/rules.d/40-monitor-hotplug.rules
 
 
index 41e27b6962ae7dc2a86bf9cd6869e822923aa5dd..548f3129767ab32b52b561dc4f0f2fd6ab4bfd79 100755 (executable)
@@ -30,6 +30,7 @@ import getopt
 import hashlib
 import os
 import posix
+import pwd
 import re
 import subprocess
 import sys
@@ -69,6 +70,7 @@ Usage: autorandr [options]
 --config                dump your current xrandr setup
 --dry-run               don't change anything, only print the xrandr commands
 --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
  directory. The script is evaluated before the screen setup is inspected, and
@@ -756,15 +758,90 @@ def exec_scripts(profile_path, script_name, meta_information=None):
 
     return all_ok
 
+def dispatch_call_to_sessions(argv):
+    """Invoke autorandr for each open local X11 session with the given options.
+
+    The function iterates over all processes not owned by root and checks
+    whether they have a DISPLAY variable set. It strips the screen from any
+    variable it finds (i.e. :0.0 becomes :0) and checks whether this display
+    has been handled already. If it has not, it forks, changes uid/gid to
+    the user owning the process, reuses the process's environment and runs
+    autorandr with the parameters from argv.
+
+    This function requires root permissions. It only works for X11 servers that
+    have at least one non-root process running. It is susceptible for attacks
+    where one user runs a process with another user's DISPLAY variable - in
+    this case, it might happen that autorandr is invoked for the other user,
+    which won't work. Since no other harm than prevention of automated
+    execution of autorandr can be done this way, the assumption is that in this
+    situation, the local administrator will handle the situation."""
+    X11_displays_done = set()
+
+    autorandr_binary = os.path.abspath(argv[0])
+
+    for directory in os.listdir("/proc"):
+        directory = os.path.join("/proc/", directory)
+        if not os.path.isdir(directory):
+            continue
+        environ_file = os.path.join(directory, "environ")
+        if not os.path.isfile(environ_file):
+            continue
+        uid = os.stat(environ_file).st_uid
+        if uid == 0:
+            continue
+
+        process_environ = {}
+        for environ_entry in open(environ_file).read().split("\0"):
+            if "=" in environ_entry:
+                name, value = environ_entry.split("=", 1)
+                if name == "DISPLAY" and "." in value:
+                    value = value[:value.find(".")]
+                process_environ[name] = value
+        display = process_environ["DISPLAY"] if "DISPLAY" in process_environ else None
+
+        if display and display not in X11_displays_done:
+            try:
+                pwent = pwd.getpwuid(uid)
+            except KeyError:
+                # User has no pwd entry
+                continue
+
+            print("Running autorandr as %s for display %s" % (pwent.pw_name, display))
+            child_pid = os.fork()
+            if child_pid == 0:
+                # This will throw an exception if any of the privilege changes fails,
+                # so it should be safe. Also, note that since the environment
+                # is taken from a process owned by the user, reusing it should
+                # not leak any information.
+                os.setgroups([])
+                os.setresgid(pwent.pw_gid, pwent.pw_gid, pwent.pw_gid)
+                os.setresuid(pwent.pw_uid, pwent.pw_uid, pwent.pw_uid)
+                os.chdir(pwent.pw_dir)
+                os.environ.clear()
+                os.environ.update(process_environ)
+                os.execl(autorandr_binary, autorandr_binary, *argv[1:])
+                os.exit(1)
+            os.waitpid(child_pid, 0)
+
+            X11_displays_done.add(display)
+
 def main(argv):
     try:
-        options = dict(getopt.getopt(argv[1:], "s:r:l:d:cfh", [ "dry-run", "change", "default=", "save=", "remove=", "load=", "force", "fingerprint", "config", "debug", "skip-options=", "help" ])[0])
+        options = dict(getopt.getopt(argv[1:], "s:r:l:d:cfh", [ "batch", "dry-run", "change", "default=", "save=", "remove=", "load=", "force", "fingerprint", "config", "debug", "skip-options=", "help" ])[0])
     except getopt.GetoptError as e:
         print("Failed to parse options: {0}.\n"
               "Use --help to get usage information.".format(str(e)),
               file=sys.stderr)
         sys.exit(posix.EX_USAGE)
 
+    # Batch mode
+    if "--batch" in options:
+        if ("DISPLAY" not in os.environ or not os.environ["DISPLAY"]) and os.getuid() == 0:
+            dispatch_call_to_sessions([ x for x in argv if x != "--batch" ])
+        else:
+            print("--batch mode can only be used by root and if $DISPLAY is unset")
+        return
+
     profiles = {}
     try:
         # Load profiles from each XDG config directory
index 716f771d9910c7f737d57f9e193645413bc787bf..a80926091539b6bb9c4cfe768f074e697ddd363f 100755 (executable)
@@ -3,121 +3,8 @@
 # 40autorandr: Change autorandr profile on thaw/resume
 exec > /var/log/autorandr.log 2>&1
 
-AUTORANDR="autorandr -c --default default"
-
-# Work around #44: Long user names in w
-export PROCPS_USERLEN=32
-
-find_user() {
-       # Determine user owning the display session from $1
-       D="$1"
-
-       # Prefer loginctl over all others, see bug #39
-       if [ -x "`which loginctl`" ]; then
-               # Based on http://unix.stackexchange.com/questions/203844/how-to-find-out-the-current-active-xserver-display-number/204498
-               # by SO user intelfx
-               user="$(
-               loginctl list-sessions --no-legend | while read id uid user seat; do
-                       session=$(loginctl show-session -p Display -p Active "$id")
-                       first=$(echo $session | cut -d" " -f1)
-                       second=$(echo $session | cut -d" " -f2)
-
-                       if [ -n $(echo "$first" | grep "Display") ]; then
-                               display=$first
-                               active=$second
-                       else
-                               display=$second
-                               active=$first
-                       fi
-
-                       active_value=$(echo "$active" | cut -d"=" -f2)
-                       display_value=$(echo "$display" | cut -d"=" -f2)
-
-                       if [ "$active_value" != "yes" ]; then
-                               continue
-                       fi
-
-                       if [ -n $display_value -a "$display_value" = "$D" ]; then
-                               echo $user
-                       fi
-               done
-               )"
-               if [ -n "$user" ]; then
-                       echo $user
-                       return 0
-               fi
-       fi
-
-       # Prefer w to who, see bug #39
-       if [ -x "`which w`" ]; then
-               user="`w -h | awk -vD="$D" '$2 ~ ":"D"(.[0-9])?$" || $3 ~ ":"D"(.[0-9])?$" {print $1}' | head -n1`"
-
-               if [ -z "$user" ];      then
-                       # This fallback checks if there is exactly one user (except
-                       # root) logged in an interactive session and assumes the
-                       # session belongs to him. See bug #39.
-                       user="`w -hu | awk '/^\w+/ && $1 !~ "root" { users[$1]=$1; } ENDFILE { if(asort(users) == 1) for(u in users) print users[u]; }'`"
-               fi
-       else
-               user="`who --all | awk -vD="$D" '$3 ~ ":"D"(.[0-9])?$" {print $1}' | head -1`"
-
-               if [ -z "$user" ];      then
-                       # Same fallback as above; see bug #39.
-                       user="`who -u | awk '/^\w+/ && $1 !~ "root" { users[$1]=$1; } ENDFILE { if(asort(users) == 1) for(u in users) print users[u]; }'`"
-               fi
-       fi
-
-       if [ -n "$user" ]; then
-               echo $user
-               return 0
-       fi
-
-       # If none of the above worked, check if there is only one user owning
-       # processes in $DISPLAY except root
-       #
-       # This code should be optimized somehow, but keep in mind that not all
-       # systems symlink sh -> bash!
-       OUTPUT="$(
-               for p in /proc/*; do
-                       [ -d $p ] || continue
-                       d="$(awk -v RS='\0' -F= '$1=="DISPLAY" {print $2}' $p/environ 2>/dev/null)"
-                       if [ "$d" = "$D" ]; then
-                               stat -c %U $p
-                       fi
-               done | sort | uniq | grep -v root | nl | head -n1
-       )"
-       count=$(echo $OUTPUT | awk '{print $1}')
-       user=$(echo $OUTPUT | awk '{print $2}')
-
-       if [ "$count" -eq 1 ]; then
-               echo $user
-               return 0
-       fi
-
-       return 1
-}
-
-detect_display()
-{
-       for X in /tmp/.X11-unix/X*; do
-               D="${X##/tmp/.X11-unix/X}"
-
-               user="$(find_user ":$D")"
-
-               if [ x"$user" != x"" ]; then
-                       logger "autorandr: Changing display configuration for display :$D, user '$user'"
-                       export DISPLAY=":$D"
-                       /bin/su -c "${AUTORANDR}" "$user"
-               fi
-       done
-}
-
-if grep -q systemd /proc/1/comm && [ "$2" = "udev" ]; then
-    exec /bin/systemctl start autorandr-resume.service
-fi
-
 case "$1" in
        thaw|resume)
-               detect_display
+               autorandr --batch -c --default default
                ;;
 esac
index 70dcde12c83b939c24b92bda9e2dac1c6f15db8e..871fad507b76959b1cddbfa78cbf59e228ab355c 100644 (file)
@@ -3,7 +3,7 @@ Description=autorandr resume hook
 After=sleep.target
 
 [Service]
-ExecStart=/etc/pm/sleep.d/40autorandr thaw
+ExecStart=/usr/bin/autorandr --batch -c --default default
 
 [Install]
-WantedBy=sleep.target
\ No newline at end of file
+WantedBy=sleep.target
index 7813befcb9a4f23a3a3dece5a48174836f40121f..cbea19b4df354fb060712edcad6efb859f8d5a79 100644 (file)
@@ -1 +1 @@
-ACTION=="change", SUBSYSTEM=="drm", RUN+="/etc/pm/sleep.d/40autorandr thaw udev"
+ACTION=="change", SUBSYSTEM=="drm", RUN+="/usr/bin/autorandr --batch -c --default default"