#!/usr/bin/env bash shopt -s nullglob # Utility functions ## Make setting default values a bit less awkward default() { declare -n _p=$1; shift [[ "$_p" ]] || { for v in "$@"; do _p+=( "$v" ) done } } ## Die. Why not? die() { declare code=${1:-0} [[ "$2" ]] && printf '%s\n' "$2" exit "$code" } ## Run the command and wait for it to die svc() { declare job_pid svc::cleanup() { kill -n "$service_stop_signal" "$job_pid" pid_wait "$job_pid" && { rm -f "$svc_pidfile" "$service_ready_flag" } }; trap 'svc::cleanup' TERM "$@" & job_pid=$! printf '%s' "$job_pid" > "$svc_pidfile" wait "$job_pid" svc::cleanup } ## Respawn respawn() { declare jobs job_pid respawn::cleanup() { jobs=( $(jobs -p) ) if [[ "$jobs" ]]; then kill -n 15 "${jobs[@]}" wait "${jobs[@]}" fi rm -f "$svc_pidfile" "$service_ready_flag" exit 0 }; trap 'respawn::cleanup' TERM respawn::sigpass() { declare sig=$1 pid=$2 kill -n "$sig" "$pid" } respawn::set_traps() { for s in "${service_signals[@]}"; do trap "respawn::sigpass $s \$job_pid" "$s" done }; respawn::set_traps while true; do exec "$@" & job_pid=$! while nullexec kill -n 0 "$job_pid"; do wait "$job_pid" done done } ## Run a command with its output discarded nullexec() { "$@" &>/dev/null } ## Wait for a pid to die pid_wait() { declare cnt=0 while nullexec kill -0 "$1"; do (( cnt >= (service_stop_timeout*10) )) && return 1 sleep 0.1 (( cnt++ )) done return 0 } ## See if NAME is a function is_function() { declare name=$1 name_type name_type=$( type -t "$name" ) if [[ $name_type == 'function' ]]; then return 0 fi return 1 } ## Simple timer timer() { declare cnt=0 timeout=$1 shift while ! "$@"; do (( cnt >= (timeout*10) )) && return 1 sleep 0.1 (( cnt++ )) done return 0 } ## Is a service ready? is_ready() [[ -f "$service_ready_flag" ]] ## Wait for this service to get ready wait_ready() { timer "$service_ready_timeout" is_ready } ## Depend on other services to be started depend() { declare s for s in "$@"; do if ! "$_self" "$s" qstatus; then nullexec "$_self" "$s" start || { failed_deps+=( "$s" ) return 1 } fi done } ## Create tmpfiles mktmpfiles() { declare f for f in "${service_tmpfiles[@]}"; do IFS=':' read -r f_path f_type f_args <<< "$f" if ! [[ -e $f_path ]]; then case "$f_type" in symlink) ln -s "$f_args" "$f_path";; file|dir) IFS=':' read -r f_perms f_owner f_group <<< "$f_args" if [[ $f_type == 'file' ]]; then > "$f_path" chmod "${f_perms:-644}" "$f_path" elif [[ "$f_type" == 'dir' ]]; then mkdir -p -m "${f_perms:-755}" "$f_path" fi if [[ "$f_owner" || "$f_group" ]]; then chown "${f_owner:-root}:${f_group:-root}" "$f_path" fi;; esac fi done } ## Depend on other services to be ready depend_ready() { declare s depend "$@" || return 1 for s in "$@"; do "$_self" "$s" wait_ready || { failed_deps+=( "$s" ) return 1 } done } # Super functions ## Start the service, write down the svc pid super_start() { (( service_running )) && return 3 rm -f "$service_stopped_flag" [[ -f "${service_command[0]}" ]] || return 9 depend "${service_depends[@]}" || return 7 depend_ready "${service_depends_ready[@]}" || return 7 mktmpfiles || return 13 if (( service_managed )); then if (( service_respawn )); then svc respawn "${service_command[@]}" &>"$service_logfile" & else svc "${service_command[@]}" &>"$service_logfile" & fi if timer "$service_ready_timeout" ready; then printf '1' > "$service_ready_flag" else return 5 fi elif (( service_oneshot )); then "${service_command[@]}" &>"$service_logfile"; res=$? (( $res )) && return "$res" printf '1' > "$service_enabled_flag" else exec "${service_command[@]}" & fi return 0 } # A separate function for oneshot services super_oneshot() { (( service_enabled )) && return 3 } ## Reload the service ## Usually just sends HUP super_reload() { (( service_running )) || return 3 if (( service_managed )); then kill -n 1 "$service_pid" else kill -n "$service_reload_signal" "$service_pid" fi } ## Stop the service ## Returns: ## 3: Service is not running. super_stop() { if (( service_oneshot )); then (( service_enabled )) || return 3 rm -f "$service_enabled_flag" return 0 else (( service_running )) || return 3 nullexec kill -n "$service_stop_signal" "$service_pid" || return 1 pid_wait "$service_pid" || return 5 > "$service_stopped_flag" return 0 fi } info() { declare \ _status_label='Running' \ _status='no' _type='daemon' _info_items=() (( service_oneshot )) && { _status_label='Enabled' _type='oneshot' } status && _status='yes' _info_items=( "Name" "$service_name" "Type" "$_type" "$_status_label" "$_status" "Exec" "${service_command[*]} ${service_args[*]}" "Respawn" "${service_respawn:-false}" "Config path" "${service_config}" ) [[ "$_status" == 'yes' ]] && { _info_items+=( "PIDfile" "${service_pidfile:-none}" "PID" "${service_pid:-none}" ) } printf "%12s: %s\n" "${_info_items[@]}" } result() { declare rc=$1; shift declare -A msgs while (( $# )); do [[ "$2" ]] || return 1 msgs["$1"]="$2" shift 2 done [[ "${msgs[$rc]}" ]] || msgs["$rc"]="Failed!" printf '%s\n' "${msgs[$rc]}" } # Overloadable functions start() { super_start; } stop() { super_stop; } reload() { super_reload; } restart() { "$_self" "$service_name" stop "$_self" "$service_name" start } logs() { ${PAGER:-less} "$service_logfile"; } ## Status is a bit of a special case. It's talkative. status() { (( service_running )) && return 0 (( service_enabled )) && return 0 (( service_stopped )) && return 7 return 1 } ## For use in scripts qstatus() { nullexec status; } ## By default there is no ready check ready() { :; } # Code main() { # Figure out our full path case "$0" in (/*) _self=$0;; (*) _self="$PWD/$0";; esac # Needs to be global declare -g service_pid # Let's set some defaults service_managed=1 usrdir='/usr/share/ssm' if (( $UID )); then # XDG stuff default XDG_CONFIG_HOME "$HOME/.config" default XDG_RUNTIME_DIR "/run/user/$UID" service_path=( "$XDG_CONFIG_HOME/ssm/services" ) cfg_path=( "$XDG_CONFIG_HOME/ssm" ) # Warn the user of deprecated stuff. if [[ -d "$XDG_CONFIG_HOME/ssm/init.d" ]]; then printf 'WARNING: `%s` was renamed to `%s`! Please move your scripts accordingly!\n' \ "$XDG_CONFIG_HOME/ssm/init.d" \ "$XDG_CONFIG_HOME/ssm/services" >&2 service_path+=( "$XDG_CONFIG_HOME/ssm/init.d" ) fi rundir="$XDG_RUNTIME_DIR/ssm" logdir="$HOME/log/ssm" else rundir='/run/ssm' logdir='/var/log/ssm' fi # Warn the user of deprecated stuff. if [[ -d "/etc/ssm/init.d" ]]; then printf 'WARNING: `/etc/ssm/init.d` was renamed to `/etc/ssm/services`! Please move your scripts accordingly!\n' >&2 service_path+=( "/etc/ssm/init.d" ) fi # Common service path service_path+=( '/etc/ssm/services' "$rundir/services" "$usrdir/services" ) # Common config path cfg_path+=( '/etc/ssm' ) # Load custom functions for (( idx=${#cfg_path[@]}-1; idx>=0; idx-- )); do cfg_dir="${cfg_path[idx]}" for f in "$cfg_dir/functions"/*; do source "$f" || die 9 "Failed to source functions from $f" done done # Now create the needed runtime stuff for d in "$rundir" "$logdir"; do mkdir -p "$d" || die 3 "Failed to create runtime dir: $d" done # If $1 is a full path, source it. # If not, search for it in the service path. if [[ $1 == /* ]]; then service_config=$1 else for i in "${service_path[@]}"; do [[ -f "$i/$1" ]] && { service_config="$i/$1" break } done fi # Die if there is no such file [[ "$service_config" ]] || die 19 "Service not found: $1" # Service name is the basename service_name="${1##*/}" # Semi-hardcoded stuff svc_pidfile="$rundir/$service_name.pid" # Get the service defaults for p in "${cfg_path[@]}"; do [[ -f "$p/conf.d/$service_name" ]] && { source "$p/conf.d/$service_name" || die 5 "Failed to read service defaults: $p/conf.d/$service_name" break } done # Get the service config source -- "$service_config" "${@:3}" || die 7 "Failed to read the service config: $service_config" # Legacy [[ "$service_args" ]] && service_command=( "${service_command[@]}" "${service_args[@]}" ) [[ "$service_respawn" == 'true' ]] && service_respawn=1 [[ "$service_type" == 'oneshot' ]] && service_oneshot=1 (( service_oneshot )) && service_managed=0 [[ "$service_pidfile" ]] && service_managed=0 if ! (( service_managed )); then (( service_respawn )) && die 21 "Refusing to respawn a service that manages itself." fi # Service-level defaults default service_pidfile "$svc_pidfile" default service_logfile "$logdir/$service_name.log" default service_ready_flag "$rundir/$service_name.ready" default service_enabled_flag "$rundir/$service_name.enabled" default service_stopped_flag "$rundir/$service_name.stopped" default service_workdir '/' default service_stop_timeout 30 default service_ready_timeout 15 default service_stop_signal 15 default service_reload_signal 1 default service_signals 1 10 12 # Let's see if there's a PID if [[ -f "$service_pidfile" ]]; then service_pid=$(<$service_pidfile) # Let's see if it's running if nullexec kill -0 "$service_pid"; then service_running=1 fi fi # Maybe the service is enabled? if [[ -f "$service_enabled_flag" ]]; then # Yay, it is! service_enabled=1 fi # Let's see if the service was deliberately stopped if [[ -f "$service_stopped_flag" ]]; then # Ooh, it was. service_stopped=1 fi # Check if action is even defined is_function "$2" || die 17 "Function $2 is not defined for $service_name." # cd into the workdir, if defined. [[ "$service_workdir" ]] && { cd "$service_workdir" || die $? } # Run pre_$action function if is_function "pre_$2"; then "pre_$2" || { printf 'pre_%s failed!\n' "$2" die 13 } fi # Run the function "$2"; res=$? case "$2" in stop) result "$res" \ 0 "Stopped $service_name" \ 3 "$service_name is not running" \ 5 "Operation timed out" ;; start) result "$res" \ 0 "Started $service_name" \ 3 "$service_name is already running" \ 5 "Readyness check for $service_name timed out" \ 7 "Failed to start dependencies for $service_name: ${failed_deps[@]}" \ 9 "service_command does not exist: ${service_command[0]}" \ 13 "Failed to create temporary files for $service_name" ;; reload) result "$res" \ 0 "Reloaded $service_name" ;; status) if (( service_oneshot )); then result "$res" \ 0 "$service_name is enabled" \ 1 "$service_name is not enabled" else result "$res" \ 0 "$service_name is running" \ 1 "$service_name is not running" \ 7 "$service_name was stopped" fi ;; esac (( res )) && return "$res" # Run post_$action function if is_function "post_$2"; then "post_$2" || { printf 'post_%s failed!\n' "$2" die 15 } fi } main "$@"