#!/bin/sh ########################################################################### # Implements the client side of the epoptes communications protocol. # The daemon reads this file when it starts, and sends it to clients when they # connect. The clients actually source it and then wait for further commands. # # Copyright (C) 2010-2015 Alkis Georgopoulos # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # On Debian GNU/Linux systems, the complete text of the GNU General # Public License can be found in `/usr/share/common-licenses/GPL'. ########################################################################### # Dash handles signals a bit differently from bash. # We need to trap both 0 and 15 otherwise the trap function isn't called at all. # 0 is normal program termination, it's called on `service epoptes restart`. # 15 is SIGTERM, it's called when socat reaches the -T timeout. # We don't always want to relaunch though. # For user sessions, ping() aborts when X isn't running. # Root connections abort when the system is shutting down. # Also, epoptes-client kills -QUIT all the other epoptes-client processes. on_signal() { # local code # we can't do that, it resets $? code="$?" trap - 0 15 # Don't relaunch if the system is shutting down if [ "$UID" -eq 0 ] && [ -d /run/systemd/system ] && [ -x /bin/systemctl ] \ && systemctl list-jobs | grep -q 'shutdown.target[[:space:]]*start' then echo "epoptes-client got signal: $1, but system is shutting down" >&5 else echo "epoptes-client got signal: $1, relaunching..." >&5 # Restore the stdio descriptors to the previously saved 5 ( sleep 10; ping; exec epoptes-client $SERVER $PORT >&5 2>&1; ) & fi exit "$code" } # Output a message and exit with an error. # Parameters: # $1..$N = The message. die() { echo "epoptes-client ERROR: $@" >&5 trap - 0 15 exit 1 } # Some actions like broadcasting need access to the display even if they # run as root. Xorg might have been restarted though, so detect DISPLAY. ensure_display() { local vars test "$UID" -eq 0 || return 0 if xwininfo -root -size >/dev/null 2>&1; then return 0 elif vars=$(./get-display); then export $vars else return 1 fi } xprop_set() { xprop -root -f "$1" 8u -set "$1" "$2" } xprop_get() { # xprop -root UNSET_VAR echoes an error message to stdout instead of stderr xprop -root "$1" 2>/dev/null | sed 's/^[^=]*//;s/^= "//;s/"$//;s/\\"/"/' } xprop_unset() { xprop -root -remove "$1" } # Calculate, export and return a collection of useful variables. info() { local server_ip def_iface if [ -z "$cached_info" ]; then VERSION=${VERSION:-0.4.3} # Just in case the client wasn't updated test -n "$USER" || USER=$(whoami) NAME=$(getent passwd "$UID" | cut -d':' -f5 | cut -d',' -f1) test -n "$HOME" || HOME=$(getent passwd "$UID" | cut -d: -f6) if [ -n "$LTSP_CLIENT_HOSTNAME" ]; then HOSTNAME="$LTSP_CLIENT_HOSTNAME" else HOSTNAME=$(hostname) test -n "$HOSTNAME" || die "Empty hostname" fi if [ -n "$LTSP_CLIENT" ] && [ -n "$LTSP_CLIENT_MAC" ]; then # LTSP exports those vars, use them if available. MAC=$(echo "$LTSP_CLIENT_MAC" | awk '{ print tolower($1) }') IP="$LTSP_CLIENT" else server_ip=$(getent hosts "$SERVER" | awk '{ print $1; exit }') server_ip=${server_ip:-$SERVER} read def_iface IP </dev/null | sed -n -e '/"VGA/s/[^"]* "[^"]*" "[^"]*" "\([^"]*\)" .*/\1/p' | tr '\n' ' ') OS=$(uname -o) export VERSION USER NAME HOME HOSTNAME IP MAC CPU RAM VGA OS cached_info=true fi cat </dev/null; "$@" ) & test -n "$print_pid" && echo $! } # Execute a command in the background. # Parameters: # $1 = the command. execute() { local launcher # Do some logging, either in ~/.xsession-errors or on the console. echo "$(LANG=C date '+%c'), epoptes-client executing: $1" >&5 case "$1" in '') echo "Can't execute an empty command." >&5 ;; www.*) set "http://$1" launcher="xdg-open" ;; http:*|https:*|ftp:*|file:*|mailto:*) launcher="xdg-open" ;; *) if [ -e "$1" ] && ( [ ! -x "$1" ] || [ -d "$1" ] ); then launcher="xdg-open" elif which -- "$1" >/dev/null; then # Don't waste RAM for sh if it's just an executable. launcher="" fi esac # In all unhandled cases, run the command with sh -c. launcher=${launcher-sh -c} background $launcher "$1" } # Log out the connected user. logout() { ./endsession --logout } # Reboot the client. reboot() { ./endsession --reboot } # Shut down the client. shutdown() { ./endsession --shutdown } # Create a thumbshot of the user screen. # Parameters: # $1 = thumbshot width. # $2 = thumbshot height. thumbshot() { # TODO: remove compatibility fallback after a few versions local executable if [ -f "./thumbshot.py" ]; then executable=./thumbshot.py else executable=./screenshot fi if ! $executable "$1" "$2"; then BAD_THUMBSHOTS=$(($BAD_THUMBSHOTS+1)) echo "$BAD_THUMBSHOTS failed thumbshots so far!" >&5 fi } # Lock the screen. # Parameters: # $1 = seconds to keep screen locked, 0 means forever - currently ignored. # $2 = message to display to the user. lock_screen() { local pid unlock_screen # TODO: remove compatibility fallback after a few versions if [ -f "./lock_screen.py" ]; then pid=$(background -p ./lock_screen.py "$2") else pid=$(background -p ./lock-screen "$2") fi xprop_set EPOPTES_LOCK_SCREEN_PID "$pid" } # Unlock a locked screen. unlock_screen() { local pid ensure_display pid=$(xprop_get EPOPTES_LOCK_SCREEN_PID) if [ -n "$pid" ]; then kill "$pid" xprop_unset EPOPTES_LOCK_SCREEN_PID fi } # Mute the system sound. # Parameters: # $1 = seconds to keep sound muted, 0 means forever - currently ignored. mute_sound() { if [ -x /usr/bin/pactl ]; then background pactl set-sink-mute @DEFAULT_SINK@ 1 elif [ -x /usr/bin/amixer ]; then background amixer -c 0 -q sset Master mute fi } # Unute the system sound. unmute_sound() { if [ -x /usr/bin/pactl ]; then background pactl set-sink-mute @DEFAULT_SINK@ 0 elif [ -x /usr/bin/amixer ]; then background amixer -c 0 -q sset Master unmute fi } # Start a network benchmark and print its pid. # Stop it automatically after a while. # Before starting, also stop any previous benchmarks that are still running. # Parameters: # $1 = seconds to run (default=10). start_benchmark() { # benchmark_pid can't be unset in the background, but no harm done benchmark_pid=$(background -p stop_start_benchmark "$@") { sleep $((2*${1:-10}+5)); stop_benchmark "$benchmark_pid"; } & echo "$benchmark_pid" } # Helper function to avoid running stop_benchmark in foreground. # Parameters: # $1 = seconds to run. stop_start_benchmark() { test -n "$benchmark_pid" && stop_benchmark "$benchmark_pid" iperf -c "$SERVER" -r ${1:+-t "$1"} } # Manually stop a previously running benchmark. # Parameters: # $1 = pid stop_benchmark() { if [ -n "$1" ]; then kill "$1" || return 0 sleep 0.2 # For some reason, iperf -c needs 2 kills! kill "$1" || return 0 sleep 0.2 # If it's not killed by now, it's hanged kill -9 "$1" && sleep 0.2 fi 2>/dev/null } # Display some text to the user. # Parameters: # $1 = text. # $2 = title. # $3 = True/False, whether the text contains pango markup. # $4 = icon name. message() { # TODO: remove compatibility fallback after a few versions if [ -f "./message.py" ]; then background ./message.py "$@" else background ./message "$@" fi } # Connect to the server to be monitored. # Parameters: # $1 = port. get_monitored() { background x11vnc -noshm -24to32 -viewonly -connect_or_exit "$SERVER:$1" } # Connect to the server to get assistance. # Parameters: # $1 = port. # $2 = grab keyboard and mouse. get_assisted() { background x11vnc -noshm -24to32 ${2:+-grabptr -grabkbd} -connect_or_exit "$SERVER:$1" } # Deactivate the screensaver, in order for the users to watch a broadcast. reset_screensaver() { if [ -x /usr/bin/xdg-screensaver ]; then xdg-screensaver reset else xset s reset fi } # Receive a broadcasted screen from the server. # Parameters: # $1 = port. # $2 = password (encrypted as in ~/.vnc/passwd and octal-escaped). # $3 = fullscreen. receive_broadcast() { receiving_broadcast=true stop_receptions reset_screensaver if [ -z "$VNCVIEWER" ]; then # If the user installed ssvnc, prefer it over xvnc4viewer if [ -x /usr/bin/ssvncviewer ]; then VNCVIEWER=ssvncviewer elif [ -x /usr/bin/xtigervncviewer ]; then VNCVIEWER=xtigervncviewer elif [ -x /usr/bin/xvnc4viewer ]; then VNCVIEWER=xvnc4viewer fi fi printf "$2" | { sleep 0.$(($(hexdump -e \"%d\" -n 2 /dev/urandom) % 50 + 50)) if [ "$VNCVIEWER" = "ssvncviewer" ]; then exec ssvncviewer -shared -viewonly -passwd /dev/stdin \ ${3:+-fullscreen} "$SERVER:$1" elif [ "$VNCVIEWER" = "xvnc4viewer" ]; then exec ${VNCVIEWER} -Shared -ViewOnly -passwd /dev/stdin \ ${3:+-FullScreen -UseLocalCursor=0 -MenuKey F13} "$SERVER:$1" elif [ "$VNCVIEWER" = "xtigervncviewer" ]; then exec ${VNCVIEWER} -Shared -ViewOnly -passwd /dev/stdin \ ${3:+-FullScreen -MenuKey F13} "$SERVER:$1" else exec vncviewer -shared -viewonly -passwd /dev/stdin \ ${3:+-fullscreen} "$SERVER:$1" fi } >/dev/null 2>&1 & xprop_set EPOPTES_VNCVIEWER_PID "$!" } # The vnc clients should automatically exit when the server is killed. # Unfortunately, that isn't always true, so try to kill them anyway. stop_receptions() { local pid unset receiving_broadcast ensure_display pid=$(xprop_get EPOPTES_VNCVIEWER_PID) if [ -n "$pid" ]; then kill "$pid" xprop_unset EPOPTES_VNCVIEWER_PID fi } # Open a root terminal inside the X session. root_term() { background xterm -e bash -l } # Send a screen session to the server using socat. # Parameters: # $1 = port. remote_term() { local screen_params if [ "$UID" -eq 0 ]; then screen_params="bash -l" else screen_params="-l" fi background sh -c " cd sleep 1 TERM=xterm exec socat EXEC:'screen $screen_params',pty,stderr tcp:$SERVER:$1" } # Ping is called every few seconds just to make sure the connection is alive. # But currently we use it as a workaround to kill stale clients too: # Epoptes-client isn't registered as an X session client, and it doesn't # exit automatically, so tell it to exit as soon as X is unavailable. ping() { if [ "$UID" -gt 0 ]; then xprop -root -f EPOPTES_CLIENT 32c -set EPOPTES_CLIENT $$ || die "No X" fi if [ "$receiving_broadcast" = true ]; then reset_screensaver fi } # Display a message. # Parameters: # $1..$N = The message. # echo() # No need to implement it in the shell, it's embedded. # Main if [ -z "$UID" ] || [ -z "$TYPE" ] || [ -z "$SERVER" ]; then die "Required environment variables are missing." fi echo " ...done" >&5 info # Prevent dbus from getting autolaunched (issue #66) export "DBUS_SESSION_BUS_ADDRESS=${DBUS_SESSION_BUS_ADDRESS:-unix:path=/dev/null}" for i in 0 15; do trap "on_signal $i" $i done