From: Phillip Berndt Date: Fri, 16 Sep 2016 14:19:24 +0000 (+0200) Subject: Introduce --batch option to autorandr: Run autorandr for each user with an X11 session X-Git-Tag: 1.0~14 X-Git-Url: https://git.donarmstrong.com/?p=deb_pkgs%2Fautorandr.git;a=commitdiff_plain;h=bae286948aaeced03357adccfb14d1ce84a568d1 Introduce --batch option to autorandr: Run autorandr for each user with an X11 session This is an attempt to resolve #45 and it might also be a better alternative to #52, #44 and #39. --- diff --git a/Makefile b/Makefile index a312da8..10570e4 100644 --- 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 diff --git a/autorandr.py b/autorandr.py index 41e27b6..548f312 100755 --- a/autorandr.py +++ b/autorandr.py @@ -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 diff --git a/contrib/pm-utils/40autorandr b/contrib/pm-utils/40autorandr index 716f771..a809260 100755 --- a/contrib/pm-utils/40autorandr +++ b/contrib/pm-utils/40autorandr @@ -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 diff --git a/contrib/systemd/autorandr-resume.service b/contrib/systemd/autorandr-resume.service index 70dcde1..871fad5 100644 --- a/contrib/systemd/autorandr-resume.service +++ b/contrib/systemd/autorandr-resume.service @@ -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 diff --git a/contrib/udev/40-monitor-hotplug.rules b/contrib/udev/40-monitor-hotplug.rules index 7813bef..cbea19b 100644 --- a/contrib/udev/40-monitor-hotplug.rules +++ b/contrib/udev/40-monitor-hotplug.rules @@ -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"