Signed-off-by: fbt <fbt@fleshless.org>
This commit is contained in:
Jack L. Frost 2018-01-08 04:38:24 +03:00
parent a633061d30
commit 758e841e1e
1 changed files with 369 additions and 324 deletions

693
ssm
View File

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