diff --git a/ssm b/ssm index 7c2c167..50fa658 100755 --- a/ssm +++ b/ssm @@ -2,16 +2,78 @@ shopt -s nullglob # Utility functions -## Make setting default values a bit less awkward -default() { - declare -n _p=$1; shift +is_function() [[ $(type -t $1 2>/dev/null) == 'function' ]] - [[ "$_p" ]] || { - for v in "$@"; do - _p+=( "$v" ) - done - } -}; readonly -f default +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() { @@ -24,6 +86,7 @@ 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" @@ -36,7 +99,7 @@ svc() { kill -n "$service_reload_signal" "$job_pid" }; trap 'svc::reload' HUP - "$@" & job_pid=$! + "$@" & job_pid = "$!" printf '%s' "$job_pid" > "$svc_pidfile" wait "$job_pid" @@ -46,7 +109,8 @@ svc() { ## Respawn respawn() { - declare jobs job_pid + declare job_pid + var job_pid respawn::cleanup() { kill -n "$service_stop_signal" "$job_pid" @@ -58,7 +122,10 @@ respawn() { }; trap 'respawn::cleanup' TERM respawn::sigpass() { - declare sig=$1 pid=$2 + declare sig pid + var sig = "$1" + var pid = "$2" + kill -n "$sig" "$pid" } @@ -69,7 +136,7 @@ respawn() { }; respawn::set_traps while true; do - exec "$@" & job_pid=$! + exec "$@" & job_pid = "$!" while nullexec kill -n 0 "$job_pid"; do wait "$job_pid" @@ -83,39 +150,29 @@ readonly -f nullexec ## Wait for a pid to die pid_wait() { - declare cnt=0 + declare cnt + var cnt = 0 while nullexec kill -0 "$1"; do (( cnt >= (service_stop_timeout*10) )) && return 1 sleep 0.1 - (( cnt++ )) + cnt++ done return 0 }; readonly -f pid_wait -## 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 -}; readonly -f is_function - ## Simple timer timer() { - declare cnt=0 timeout=$1 + declare cnt timeout + var cnt = 0 + var timeout = "$1" shift while ! "$@"; do (( cnt >= (timeout*10) )) && return 1 sleep 0.1 - (( cnt++ )) + cnt++ done return 0 @@ -136,7 +193,7 @@ depend() { for s in "$@"; do if ! "$_self" "$s" qstatus; then nullexec "$_self" "$s" start || { - failed_deps+=( "$s" ) + failed_deps += "$s" return 1 } fi @@ -177,14 +234,14 @@ depend_ready() { for s in "$@"; do "$_self" "$s" wait_ready || { - failed_deps+=( "$s" ) + failed_deps += "$s" return 1 } done }; readonly -f depend_ready result() { - declare rc=$1; shift + declare rc; var rc = "$1"; shift declare -A msgs while (( $# )); do @@ -200,70 +257,34 @@ result() { }; readonly -f result read_systemd_service() { - declare section key value + declare section key value line + var key value line while read -r line; do - [[ $line =~ ^\[(.+)\] ]] && section=${BASH_REMATCH[1],,} - [[ $line =~ ^([^=]+)=(.+) ]] && { - key=${BASH_REMATCH[1],,} - value=${BASH_REMATCH[2]} + 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;; + (pidfile) service_pidfile = "$value";; (execstart) eval "service_command=( $value )";; (execstop) eval "stop() { $value; }";; (execreload) eval "reload() { $value; }";; - (restart) [[ $value == 'always' ]] && service_respawn=1;; + (restart) [[ $value == 'always' ]] && service_respawn = 1;; esac ;; esac done < "$1" } -# Some DSL for the config -setter() { - for i in "$@"; do - declare varname=$i - eval " - ${varname}() { - declare mode=set - declare -n _var=${varname} - - while (( \$# )); do - case \$1 in - (=) mode=set;; - (+=) mode=append;; - (_=) mode=prepend;; - (--) shift; break;; - (*) break;; - esac - shift - done - - case \$mode in - (append) _var+=( \"\$@\" );; - (prepend) _var=( \"\$@\" \"\${_var[@]}\" );; - (set) _var=( \"\$@\" );; - esac - }; readonly -f ${varname} - " - done -}; readonly setter - -setter \ - service_path \ - service_workdir \ - service_stop_timeout service_ready_timeout \ - service_stop_signal service_reload_signal service_signals \ - systemd systemd_service_path - # Overloadable functions ## Start the service, write down the svc pid start() { - (( service_running )) && return 3 + service_running && return 3 rm -f "$service_stopped_flag" @@ -274,8 +295,8 @@ start() { mktmpfiles || return 13 - if (( service_managed )); then - if (( service_respawn )); then + if service_managed; then + if service_respawn; then svc respawn "${service_command[@]}" &>"$service_logfile" & else svc "${service_command[@]}" &>"$service_logfile" & @@ -286,9 +307,9 @@ start() { else return 5 fi - elif (( service_oneshot )); then + elif service_oneshot; then "${service_command[@]}" &>"$service_logfile"; res=$? - (( $res )) && return "$res" + (( res )) && return "$res" printf '1' > "$service_enabled_flag" else exec "${service_command[@]}" & @@ -300,7 +321,7 @@ start() { ## Reload the service ## Usually just sends HUP reload() { - (( service_running )) || return 3 + service_running || return 3 kill -n "$service_reload_signal" "$service_pid" } @@ -309,14 +330,14 @@ reload() { ## Returns: ## 3: Service is not running. stop() { - if (( service_oneshot )); then - (( service_enabled )) || return 3 + if service_oneshot; then + service_enabled || return 3 rm -f "$service_enabled_flag" return 0 else - (( service_running )) || return 3 + service_running || return 3 nullexec kill -n "$service_stop_signal" "$service_pid" || return 1 @@ -328,34 +349,32 @@ stop() { } info() { - declare \ - _status_label='Running' \ - _status='no' - _type='daemon' - _info_items=() + 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' + service_oneshot && { + _status_label = 'Enabled' + _type = 'oneshot' } - status && _status='yes' + 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}" - ) + _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}" + if _status == 'yes'; then + _info_items += \ + "PIDfile" "${service_pidfile:-none}" \ "PID" "${service_pid:-none}" - ) - } + fi printf "%12s: %s\n" "${_info_items[@]}" } @@ -366,8 +385,8 @@ restart() { "$_self" "$service_name" start } -edit() { "${EDITOR:-vim}" "$service_config"; } -logs() { ${PAGER:-less} "$service_logfile"; } +edit() { $EDITOR "$service_config"; } +logs() { $PAGER "$service_logfile"; } ## Status is a bit of a special case. It's talkative. status() { @@ -385,234 +404,260 @@ qstatus() { nullexec status; } ready() { :; } # Main code -main() { - # Figure out our full path - case "$0" in - (/*) _self=$0;; - (*) _self="$PWD/$0";; - esac +# Figure out our full path +case "$0" in + (/*) var _self = "$0";; + (*) var _self = "$PWD/$0";; +esac - # Needs to be global - declare -g service_pid +# check for some environment stuff +var EDITOR := 'vim' +var PAGER := 'less' - # Let's set some defaults - service_managed=1 - usrdir='/usr/share/ssm' - systemd_service_path=( /etc/systemd/system /run/systemd/system /lib/systemd/system ) +# Empty declarations +var service_pid +var service_pidfile +var service_type +var service_depends_ready +var service_command +var service_config +var service_path +var service_name +var service_args +var service_logfile +var service_ready_flag +var service_enabled_flag +var service_stopped_flag +var failed_deps +var svc_pidfile +var cfg_path +var cfg_dir +var rundir +var logdir - # XDG stuff - default XDG_CONFIG_HOME "$HOME/.config" - default XDG_RUNTIME_DIR "/run/user/$UID" +# Let's set some defaults +var service_managed = 0 +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' - if (( $UID )); then - rundir="$XDG_RUNTIME_DIR/ssm" - logdir="$HOME/log/ssm" - else - rundir='/run/ssm' - logdir='/var/log/ssm' +# XDG stuff +var XDG_CONFIG_HOME := "$HOME/.config" +var XDG_RUNTIME_DIR := "/run/user/$UID" + +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 - # 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 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' - # 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]}" - # 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 - } + 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" ]] && { - service_name=$1 - service_systemd=1 - 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_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 - 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 + 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 } - fi + done - # Run the function - "$2"; res=$? + 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" - 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 + read_systemd_service "$i" + break + } + done } - fi -}; readonly -f main + } +fi -main "$@" +# 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