Commit 4da7ffe7 authored by Seblu's avatar Seblu
Browse files

checkservices: Multiple code refactoring

- move code to bash function
- use uppercase for global vars
- drop dbus restart code when updated
- split broken maps and dbus lookup
- change coloring code
parent 8ea1bc5b
Loading
Loading
Loading
Loading
+190 −150
Original line number Diff line number Diff line
@@ -19,62 +19,43 @@
# Check running systemd services for binary update
# Convenient way to restart updated systemd service after upgrade

# bash options
shopt -s xpg_echo

# disable grep options to avoid non default behaviour
unset GREP_OPTIONS

# Systemd cgroup path
# systemd cgroup path
SYSTEMD_CGROUP_BASE_PATH='/sys/fs/cgroup/systemd'

# colors
if [[ -t 1 ]]; then
    shopt -s xpg_echo
    c_arrow='\e[1;34m'
    c_title='\e[1;37m'
    c_svc='\e[1;35m'
    c_warn='\e[5;30;43m'
    c_error='\e[1;31m'
    c_rst='\e[m'
    C_BOLD='\e[1m'
    C_BLUE='\e[34m'
    C_RED='\e[31m'
    C_WHITE='\e[37m'
    C_RESET='\e[m'
fi

# default options
autoconfirm=0       # autoconfirmation
dbus=1              # relauch when dbus
debug=0             # debug mode
failed=1            # display failed service at the end
pacdiff=1           # run pacdiff
reload=1            # reload systemd
restart=1           # restart services
serialize=0         # run in parallel
status=1            # display status after systemctl
user_slice=0        # act on users services

# display application usage and exit 2
usage() {
    echo "usage ${0##*/} [options]"
    echo "description: check for updated files in a service"
    echo 'options:'
    echo '  -h: this help' >&2
    echo "  -d: debug mode" >&2
    echo "  -c: auto confirmation" >&2
    echo "  -b/-B: restart (or not) ${0##*/} if dbus was updated (default: $dbus)" >&2
    echo "  -l/-L: call (or not) systemd daemon-reload (default: $reload)" >&2
    echo "  -f/-F: display (or not) failed services before quit (default: $failed)" >&2
    echo "  -p/-P: call (or not) pacdiff before act (default: $pacdiff)" >&2
    echo "  -r/-R: restart (or not) services with updated files (default: $restart)" >&2
    echo "  -s/-S: display (or not) status of restarted service (default: $status)" >&2
    echo "  -u/-U: act (or not) on services in users slice (default: $user_slice)" >&2
    echo "  -z/-Z: serialize (or not) action (default: $serialize)" >&2
    exit 2
}
AUTOCONFIRM=0       # autoconfirmation
DBUS=1              # relauch when dbus
FAILED=1            # display failed service at the end
PACDIFF=1           # run pacdiff
RELOAD=1            # reload systemd
RESTART=1           # restart services
SERIALIZE=0         # run in parallel
STATUS=1            # display status after systemctl
USER_SLICE=0        # act on users services

# print $* as an arrow line
arrow() {
    printf "%b==> %b%s%b\n" "$c_arrow" "$c_title" "$*" "$c_rst"
    printf "${C_BOLD}${C_BLUE}:: ${C_WHITE}%s${C_RESET}\n" "$*"
}

# print $* as an error message
error() {
    printf "%bError: %b%s%b\n" "$c_error" "$c_title" "$*" "$c_rst" >&2
    printf "${C_BOLD}${C_RED}Error:: ${C_WHITE}%s${C_RESET}\n" "$*" >&2
}

# usage : in_array( $needle, $haystack )
@@ -92,7 +73,7 @@ in_array() {
# ask for confirmation
# return 0 when confirmed, otherwise 1
confirm() {
    (( $autoconfirm == 1 )) && return 0
    (( $AUTOCONFIRM == 1 )) && return 0
    local -i try
    local ans
    for try in 5 4 3 2 1; do
@@ -107,118 +88,96 @@ confirm() {
    return 1
}

while getopts 'ahBbdFfLlPpRrSsUuZz' opt; do
    case $opt in
        a) autoconfirm=0;;
        B) dbus=0;;             b) dbus=1;;
        d) debug=1;;
        F) failed=0;;           f) failed=1;;
        L) reload=0;;           l) reload=1;;
        P) pacdiff=0;;          p) pacdiff=1;;
        R) restart=0;;          r) restart=1;;
        S) status=0;;           s) status=1;;
        U) user_slice=0;;       u) user_slice=1;;
        Z) serialize=0;;        z) serialize=1;;
        *) usage;;
    esac
done
shift $((OPTIND - 1));
(( $# > 0 )) && usage

# avoid to be sighup'ed by interactive shell
trap '' SIGHUP

# from now, we need to be root
(( $UID != 0 )) && error 'You need to be root' && exit 1
# get running systemd services
get_services() {
    systemctl --no-legend --full --type service --state running|cut -f1 -d' '
}

# call pacdiff
(( $pacdiff )) && arrow 'Run pacdiff' && pacdiff

# reload units list
(( $reload )) && arrow 'Reload systemd' && systemctl --system daemon-reload

# list of running services
arrow 'List runnings systemd services'
declare -a services
services=($(systemctl --no-legend --full --type service --state running|cut -f1 -d' '))

# list of bus names
arrow 'List Dbus clients'
declare -a buses
buses=($(dbus-send --system --dest=org.freedesktop.DBus --type=method_call \--print-reply \
/org/freedesktop/DBus org.freedesktop.DBus.ListNames|sed -rn 's/\s*string "(.*)"/\1/p'))

# count beggar services
arrow "Search for updated mapped files"
declare -a needy=() pids=()
declare -i pid=0
for svc in "${services[@]}"; do
    unit_path="$(systemctl -p ControlGroup show "$svc"|cut -f 2 -d=)"
    busname="$(systemctl -p BusName show "$svc"|cut -f 2 -d=)"
# get systemd services with updated mapped files
get_broken_maps() {
    local service path pidfile unit_path maps_path pids deleted
    local -a pids=()
    local -i pid=0
    for service in $(get_services); do
        unit_path="$(systemctl -p ControlGroup show "$service"|cut -f 2 -d=)"
        # get the right pidfile name
    unset pidfile
        pidfile=''
        for path in "$SYSTEMD_CGROUP_BASE_PATH$unit_path/cgroup.procs" \
            "$SYSTEMD_CGROUP_BASE_PATH$unit_path/tasks"; do
            [[ -r "$path" ]] && pidfile="$path" && continue
        done
    [[ -z "$pidfile" ]] && error "Unable to find pid file for $svc." && continue
        [[ -z "$pidfile" ]] && error "Unable to find pid file for $service." && continue
        # skip non system units
    (( $user_slice == 0 )) && [[ "$unit_path" =~ /user\.slice/ ]] && continue
        (( $USER_SLICE == 0 )) && [[ "$unit_path" =~ /user\.slice/ ]] && continue
        # parse pidfile
        pids=( $(< "$pidfile") )
        if (( "${#pids[*]}" == 0 )); then
        error "Unable to parse pid file for $svc."
            error "Unable to parse pid file for $service."
            continue
        fi
        for pid in "${pids[@]}"; do
            maps_path="/proc/$pid/maps"
            [[ -r "$maps_path" ]] || {
            error "Unable to read maps file of $svc for pid $pid."
                error "Unable to read maps file of $service for pid $pid."
                continue
            }
        deleted=$(grep -F '(deleted)' "$maps_path" |sed -nr 's|^\S+ ..x. \S+ \S+ \S+ \s+||p'|sort|uniq)
        if [[ -n $deleted ]] || { [[ -n "$busname" ]] && ! in_array "$busname" "${buses[@]}"; }; then
            needy+=("$svc")
            if (( $debug )); then
                echo "${c_title}Service:${c_svc} $svc${c_rst}"
                echo "${c_title}Pid:${c_rst} $pid"
                echo "${c_title}Bus:${c_rst} $busname"
                echo -n "${c_title}BusName matching on the system bus: ${c_rst}"
                in_array "$busname" "${buses[@]}" && echo 'Yes' || echo 'No'
                echo "${c_title}Deleted files:${c_rst}"
                echo "$deleted"
                echo
            fi
            # only file mapped as executable
            deleted="$(grep -F '(deleted)' "$maps_path"|sed -nr 's|^\S+ ..x. \S+ \S+ \S+ \s+||p')"
            if [[ $deleted ]]; then
                printf "%s\n" $service
                break
            fi
        done
    done
}

# get dbus clients on the system bus
get_dbus_names() {
    dbus-send --system --dest=org.freedesktop.DBus --type=method_call --print-reply \
    /org/freedesktop/DBus org.freedesktop.DBus.ListNames|sed -rn 's/\s*string "(.*)"/\1/p'
}

# display what we will do
(( "${#needy[*]}" )) && echo '-------8<-------------------------------8<---------'
for svc in "${needy[@]}"; do
    echo "systemctl restart '$svc'"
# get systemd services not registered on dbus system bus
get_missing_dbus() {
    local service busname
    local -a registered=($(get_dbus_names))
    for service in $(get_services); do
        # get the service registered bus name
        busname="$(systemctl -p BusName show "$service"|cut -f 2 -d=)"
        if [[ "$busname" ]] && ! in_array "$busname" "${registered[@]}"; then
            echo $service
        fi
    done
(( "${#needy[*]}" )) && echo '-------8<-------------------------------8<---------'
}

# start the dangerous action below
if (( $restart == 1 && ${#needy[*]} > 0 )) && confirm 'Confirm service restart?'; then
# display restart intruction from service name
display_restart() {
    local service
    echo '-------8<-------------------------------8<---------'
    for service; do
        echo "systemctl restart '$service'"
    done
    echo '-------8<-------------------------------8<---------'
}

    declare -A registered_pids=()
    declare -a running_pids=()
    declare -i last_registered_pids_count
# restart systemd services given in arguments
restart_services() {
    local service
    local -i last_registered_pids_count
    local -A registered_pids=()
    local -a running_pids=()

    # do the job, restart updated services
    for svc in "${needy[@]}"; do
        echo "Restarting ${c_svc}$svc${c_rst}"
        systemctl restart "$svc" &
        if (( $serialize )); then
    for service; do
        echo "systemctl restart $service"
        systemctl restart "$service" &
        if (( $SERIALIZE )); then
            wait
            # display status directly when serialize and not quiet
            (( $status )) && systemctl --no-pager --lines=0 status "$svc"
            (( $STATUS )) && systemctl --no-pager --lines=0 status "$service"
        else
            # register pids
            registered_pids[$!]="$svc"
            registered_pids[$!]="$service"
        fi
    done

@@ -235,7 +194,7 @@ if (( $restart == 1 && ${#needy[*]} > 0 )) && confirm 'Confirm service restart?'
        for pid in "${!registered_pids[@]}"; do
            in_array "$pid" "${running_pids[@]}" && continue
            # show units status
            (( $status )) && systemctl --no-pager --lines=0 status "${registered_pids[$pid]}"
            (( $STATUS )) && systemctl --no-pager --lines=0 status "${registered_pids[$pid]}"
            unset registered_pids[$pid]
            break
        done
@@ -249,18 +208,99 @@ if (( $restart == 1 && ${#needy[*]} > 0 )) && confirm 'Confirm service restart?'
            break
        fi
    done
}


# display application usage and exit 2
usage() {
    echo "usage ${0##*/} [options]"
    echo "description: check for updated files in a service"
    echo 'options:'
    echo '  -h: this help' >&2
    echo "  -c: auto confirmation" >&2
    echo "  -l/-L: call (or not) systemd daemon-reload (default: $RELOAD)" >&2
    echo "  -f/-F: display (or not) failed services before quit (default: $FAILED)" >&2
    echo "  -p/-P: call (or not) pacdiff before act (default: $PACDIFF)" >&2
    echo "  -r/-R: restart (or not) services with updated files (default: $RESTART)" >&2
    echo "  -s/-S: display (or not) status of restarted service (default: $STATUS)" >&2
    echo "  -u/-U: act (or not) on services in users slice (default: $USER_SLICE)" >&2
    echo "  -z/-Z: serialize (or not) action (default: $SERIALIZE)" >&2
    exit 2
}

# parse command line arguments
# set options as global vars
argparse() {
    local opt
    while getopts 'ahFfLlPpRrSsUuZz' opt; do
        case $opt in
            a) AUTOCONFIRM=0;;
            F) FAILED=0;;           f) FAILED=1;;
            L) RELOAD=0;;           l) RELOAD=1;;
            P) PACDIFF=0;;          p) PACDIFF=1;;
            R) RESTART=0;;          r) RESTART=1;;
            S) STATUS=0;;           s) STATUS=1;;
            U) USER_SLICE=0;;       u) USER_SLICE=1;;
            Z) SERIALIZE=0;;        z) SERIALIZE=1;;
            *) usage;;
        esac
    done
    shift $((OPTIND - 1));
    (( $# > 0 )) && usage
}

    # warn if dbus was restart
    if in_array dbus.service "${needy[@]}"; then
        echo "${c_warn}After dbus restart, you should run ${0##*/} twice${c_rst}" >&2
        if (( $dbus )) && [[ -z "$CHECKSERVICE_WAS_RESTARTED" ]]; then
            echo "${c_warn}Doing it for you. No need to thanks me!${c_rst}" >&2
            export CHECKSERVICE_WAS_RESTARTED=1
            exec "$0" "$@"
# emulated program entry point
main() {
    # avoid to be sighup'ed by interactive shell
    trap '' SIGHUP

    # from now, we need to be root
    (( $UID != 0 )) && error 'You need to be root' && exit 1

    # parse command line options
    argparse "$@"

    # call pacdiff to ensure config files are updated before restart
    if (( $PACDIFF )); then
        arrow 'Run pacdiff'
        pacdiff
    fi

    # ensure systemd has been reloaded
    if (( $RELOAD )); then
        arrow 'Reload systemd'
        systemctl --system daemon-reload
    fi

    arrow 'Services with broken maps files'
    local -a broken_services=($(get_broken_maps))
    echo "Found: ${#broken_services[@]}"
    if (( ${#broken_services[@]} )); then
        display_restart "${broken_services[@]}"
        if confirm 'Execute?'; then
            arrow 'Restart broken services'
            restart_services "${broken_services[@]}"
        fi
    fi

    arrow 'Services missing on the system bus'
    local -a missing_services=($(get_missing_dbus))
    echo "Found: ${#missing_services[@]}"
    if (( ${#missing_services[@]} )); then
        display_restart "${missing_services[@]}"
        if confirm 'Execute?'; then
            arrow 'Restart missing services'
            restart_services "${missing_services[@]}"
        fi
    fi

    # list only failed systemd units
    if (( $FAILED )); then
        arrow "List failed units"
        systemctl --failed --all --no-pager --no-legend --full list-units
    fi
}

(( $failed )) && arrow "List failed units" && systemctl --failed --all --no-pager --no-legend --full list-units
main "$@"

exit 0