#!/usr/bin/env bash shopt -s nullglob # Utility functions is_function() [[ $(type -t $1 2>/dev/null) == 'function' ]] var() { declare varname=$1; shift if ! is_function "$varname"; then eval " ${varname}() { declare mode=set declare -n _var=\"${varname}\" if (( \$# )); then case \"\$1\" in ('=') mode=set;; ('+=') mode=append;; ('_=') mode=prepend;; (':=') mode=default;; ('==') mode=compare;; ('=~') mode=regex;; (*) die 71 \"Syntax error in \$FUNCNAME!\";; esac shift else mode='bool' fi case \"\$mode\" in (set) _var=( \"\$@\" );; (append) _var+=( \"\$@\" );; (prepend) _var=( \"\$@\" \"\${var[@]}\" );; (default) [[ \"\$_var\" ]] || _var=( \"\$@\" );; (compare) [[ \"\$_var\" == \"\$*\" ]];; (regex) [[ \"\$_var\" =~ \$@ ]];; (bool) case \"\${_var,,}\" in (''|'false'|'0') return 1;; (*) return 0;; esac ;; esac }; readonly -f \"${varname}\" ${varname}++() { declare -n _var=\"${varname}\" (( ${varname}++ )) } ${varname}--() { declare -n _var=\"${varname}\" (( ${varname}-- )) } " fi if (( $# )); then case "$1" in ('='|'=='|'=~'|'+='|'_='|':=') "$varname" "$@" ;; (*) for v in "$@"; do var "$v" done ;; esac fi } ## Die. Why not? die() { declare code=${1:-0} [[ "$2" ]] && printf '%s\n' "$2" exit "$code" }; readonly -f die ## Run the command and wait for it to die svc() { declare job_pid var 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 svc::reload() { kill -n "$service_reload_signal" "$job_pid" }; trap 'svc::reload' HUP "$@" & job_pid = "$!" printf '%s' "$job_pid" > "$svc_pidfile" wait "$job_pid" svc::cleanup }; readonly -f svc ## Respawn respawn() { declare job_pid var job_pid respawn::cleanup() { kill -n "$service_stop_signal" "$job_pid" wait "$job_pid" rm -f "$svc_pidfile" "$service_ready_flag" exit 0 }; trap 'respawn::cleanup' TERM respawn::sigpass() { declare sig pid var sig = "$1" var 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 }; readonly -f respawn ## Run a command with its output discarded nullexec() { "$@" &>/dev/null; } readonly -f nullexec ## Wait for a pid to die pid_wait() { declare cnt var cnt = 0 while nullexec kill -0 "$1"; do (( cnt >= (service_stop_timeout*10) )) && return 1 sleep 0.1 cnt++ done return 0 }; readonly -f pid_wait ## Simple timer timer() { declare cnt timeout var cnt = 0 var timeout = "$1" shift while ! "$@"; do (( cnt >= (timeout*10) )) && return 1 sleep 0.1 cnt++ done return 0 }; readonly -f timer ## Is a service ready? is_ready() [[ -f "$service_ready_flag" ]] readonly -f is_ready ## Wait for this service to get ready wait_ready() { timer "$service_ready_timeout" is_ready; } readonly -f wait_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 }; readonly -f depend ## 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 }; readonly -f mktmpfiles ## 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 }; readonly -f depend_ready result() { declare rc; var 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]}" }; readonly -f result read_systemd_service() { declare section key value line var key value line while read -r line; do line =~ '^\[(.+)\]' && var section = "${BASH_REMATCH[1],,}" line =~ '^([^=]+)=(.+)' && { key = "${BASH_REMATCH[1],,}" value = "${BASH_REMATCH[2]}" } case $section in (service) case $key in (pidfile) service_pidfile = "$value";; (execstart) eval "service_command=( $value )";; (execstop) eval "stop() { $value; }";; (execreload) eval "reload() { $value; }";; (restart) [[ $value == 'always' ]] && service_respawn = 1;; esac ;; esac done < "$1" } # Overloadable functions ## Start the service, write down the svc pid 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 } ## Reload the service ## Usually just sends HUP reload() { service_running || return 3 kill -n "$service_reload_signal" "$service_pid" } ## Stop the service ## Returns: ## 3: Service is not running. 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 _status _type _info_items var _status_label = 'Running' var _status = 'no' var _type = 'daemon' var _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}" \ if _status == 'yes'; then _info_items += \ "PIDfile" "${service_pidfile:-none}" \ "PID" "${service_pid:-none}" fi printf "%12s: %s\n" "${_info_items[@]}" } ## Restart just calls the script twice by default restart() { "$_self" "$service_name" stop "$_self" "$service_name" start } edit() { $EDITOR "$service_config"; } logs() { $PAGER "$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() { :; } # Main code ## Empty declarations var service_pid \ service_pidfile \ service_type \ service_depends_ready \ service_command \ service_config \ service_path \ service_name \ service_args \ service_logfile \ service_ready_flag \ service_enabled_flag \ service_stopped_flag \ failed_deps \ svc_pidfile \ cfg_path \ cfg_dir \ rundir \ logdir \ _self ## check for some environment stuff var EDITOR := 'vim' var PAGER := 'less' var XDG_CONFIG_HOME := "$HOME/.config" var XDG_RUNTIME_DIR := "/run/user/$UID" ## Let's set some defaults var service_managed = 1 var service_respawn = 0 var service_oneshot = 0 var service_running = 0 var service_enabled = 0 var service_stopped = 0 var service_systemd = 0 var service_workdir = '/' var service_stop_timeout = 30 var service_ready_timeout = 15 var service_stop_signal = 15 var service_reload_signal = 1 var service_signals = 1 10 12 var systemd = 0 var systemd_service_path = /etc/systemd/system /run/systemd/system /lib/systemd/system var ssm_config = 0 var usrdir = '/usr/share/ssm' ## Figure out our full path case "$0" in (/*) _self = "$0";; (*) _self = "$PWD/$0";; esac if (( $UID )); then rundir = "$XDG_RUNTIME_DIR/ssm" logdir = "$HOME/log/ssm" else rundir = '/run/ssm' logdir = '/var/log/ssm' fi # Warn the user of deprecated stuff. for p in "/etc/ssm/init.d" "$XDG_CONFIG_HOME/ssm/init.d"; do if [[ -d "$p" ]]; then printf 'WARNING: `%s` was renamed to `%s`! Please move your scripts accordingly!\n' "$p" "${p%init.d}services" >&2 service_path += "$p" fi done # Common service path service_path += "$XDG_CONFIG_HOME/ssm/services" '/etc/ssm/services' "$rundir/services" "$usrdir/services" # Common config path cfg_path = "$XDG_CONFIG_HOME/ssm" '/etc/ssm' # Load custom config and functions, reversing the PATH order for (( idx=${#cfg_path[@]}-1; idx>=0; idx-- )); do cfg_dir = "${cfg_path[idx]}" ssm_config || { [[ -f "$cfg_dir/ssm.conf" ]] && { source "$cfg_dir/ssm.conf" || die 37 "Failed to load config: $cfg_dir/ssm.conf" ssm_config = 1 } } 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[@]/%//$1}"; do [[ -f "$i" ]] && { service_config = "$i" break } done service_config || { # Search for a systemd service too systemd && { for i in "${systemd_service_path[@]/%//$1.service}"; do [[ -f "$i" ]] && { var service_name = "$1" var service_systemd = 1 var service_config = "$i" read_systemd_service "$i" break } done } } fi # Die if there is no service config file service_config || die 19 "Service not found: $1" # We can handle other people's service configs, poorly if service_systemd; then # I'm pretty sure we'll need this : else # Service name is the basename service_name = "${1##*/}" # Get the service defaults for p in "${cfg_path[@]/%//$service_name}"; do [[ -f "$p" ]] && { source "$p" || die 5 "Failed to read service defaults: $p" } done # Get the service config source -- "$service_config" "${@:3}" || die 7 "Failed to read the service config: $service_config" fi # Legacy service_args && 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 # Semi-hardcoded stuff svc_pidfile = "$rundir/$service_name.pid" # Service-level defaults service_pidfile := "$svc_pidfile" service_logfile := "$logdir/$service_name.log" service_ready_flag := "$rundir/$service_name.ready" service_enabled_flag := "$rundir/$service_name.enabled" service_stopped_flag := "$rundir/$service_name.stopped" # 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