Skip to content

Commit

Permalink
Introduce --batch option to autorandr: Run autorandr for each user wi…
Browse files Browse the repository at this point in the history
…th an X11 session

This is an attempt to resolve #45 and it might also be a better
alternative to #52, #44 and #39.
  • Loading branch information
phillipberndt committed Sep 16, 2016
1 parent 3c12874 commit bae2869
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 122 deletions.
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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


Expand Down
79 changes: 78 additions & 1 deletion autorandr.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import hashlib
import os
import posix
import pwd
import re
import subprocess
import sys
Expand Down Expand Up @@ -69,6 +70,7 @@
--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
Expand Down Expand Up @@ -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

This comment has been minimized.

Copy link
@blueyed

blueyed Sep 17, 2016

Contributor

What about using ps e to get the environment vars?

This comment has been minimized.

Copy link
@phillipberndt

phillipberndt Sep 19, 2016

Author Owner

The ps e output is much harder to parse than the envioron file in procfs. (It is, in fact, impossible to parse, because it doesn't perform any quoting/escaping in its output. It'll have identical output for an environment foo="bar baz=qux" and one foo=bar baz=qux. For foo="bar=", parsing necessarily fails for all consecutive variables.)

So this wouldn't make this part of the script more reliable.


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
Expand Down
115 changes: 1 addition & 114 deletions contrib/pm-utils/40autorandr
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions contrib/systemd/autorandr-resume.service
Original file line number Diff line number Diff line change
Expand Up @@ -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
WantedBy=sleep.target
2 changes: 1 addition & 1 deletion contrib/udev/40-monitor-hotplug.rules
Original file line number Diff line number Diff line change
@@ -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"

0 comments on commit bae2869

Please sign in to comment.