Files
ssm/ssm
T

553 lines
11 KiB
Bash
Raw Normal View History

2016-11-13 11:11:47 +03:00
#!/usr/bin/env bash
shopt -s nullglob
# Utility functions
## Make setting default values a bit less awkward
default() {
2017-12-08 14:39:50 +03:00
declare -n _p=$1; shift
[[ "$_p" ]] || {
for v in "$@"; do
_p+=( "$v" )
done
}
2016-11-13 11:11:47 +03:00
}
## 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() {
2016-11-13 16:31:17 +03:00
declare job_pid
2016-11-13 11:11:47 +03:00
svc::cleanup() {
2016-11-13 16:31:17 +03:00
kill -n "$service_stop_signal" "$job_pid"
2016-11-13 11:11:47 +03:00
2016-11-13 17:33:58 +03:00
pid_wait "$job_pid" && {
2017-03-11 17:36:07 +03:00
rm -f "$svc_pidfile" "$service_ready_flag"
2016-11-13 11:11:47 +03:00
}
2017-07-13 15:37:53 +03:00
}; trap 'svc::cleanup' TERM
2016-11-13 11:11:47 +03:00
2016-11-13 16:31:17 +03:00
"$@" & job_pid=$!
2016-11-13 14:47:20 +03:00
2016-11-13 17:33:58 +03:00
printf '%s' "$job_pid" > "$svc_pidfile"
wait "$job_pid"
2017-03-11 17:36:07 +03:00
svc::cleanup
2016-11-13 11:11:47 +03:00
}
2016-11-13 14:47:20 +03:00
## Respawn
2016-11-13 11:11:47 +03:00
respawn() {
2016-11-13 16:31:17 +03:00
declare jobs job_pid
2016-11-13 11:11:47 +03:00
respawn::cleanup() {
2016-11-13 16:31:17 +03:00
jobs=( $(jobs -p) )
if [[ "$jobs" ]]; then
kill -n 15 "${jobs[@]}"
wait "${jobs[@]}"
fi
2017-03-11 17:37:31 +03:00
rm -f "$svc_pidfile" "$service_ready_flag"
2017-03-11 17:36:07 +03:00
2016-11-13 11:11:47 +03:00
exit 0
2016-11-13 14:47:20 +03:00
}; trap 'respawn::cleanup' TERM
2016-11-13 11:11:47 +03:00
2016-11-13 17:33:58 +03:00
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
2016-11-13 11:11:47 +03:00
while true; do
2016-11-13 16:31:17 +03:00
exec "$@" & job_pid=$!
2016-11-13 17:33:58 +03:00
while nullexec kill -n 0 "$job_pid"; do
wait "$job_pid"
done
2016-11-13 11:11:47 +03:00
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
2016-11-13 14:47:20 +03:00
(( cnt >= (service_stop_timeout*10) )) && return 1
sleep 0.1
2016-11-13 11:11:47 +03:00
(( cnt++ ))
done
return 0
}
## See if NAME is a function
is_function() {
2017-07-13 16:20:57 +03:00
declare name=$1 name_type
2016-11-13 11:11:47 +03:00
name_type=$( type -t "$name" )
if [[ $name_type == 'function' ]]; then
return 0
fi
return 1
}
2016-11-13 14:47:20 +03:00
## Simple timer
timer() {
2017-07-13 16:25:41 +03:00
declare cnt=0 timeout=$1
2016-11-13 14:47:20 +03:00
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
2017-12-14 07:07:48 +03:00
if ! "$_self" "$s" qstatus; then
nullexec "$_self" "$s" start || {
2016-11-13 14:47:20 +03:00
failed_deps+=( "$s" )
return 1
}
fi
done
}
2016-11-13 17:33:58 +03:00
## 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
}
2016-11-13 14:47:20 +03:00
## Depend on other services to be ready
depend_ready() {
declare s
depend "$@" || return 1
for s in "$@"; do
2017-12-14 07:07:48 +03:00
"$_self" "$s" wait_ready || {
2016-11-13 14:47:20 +03:00
failed_deps+=( "$s" )
return 1
}
done
2016-11-13 11:11:47 +03:00
}
# Super functions
## Start the service, write down the svc pid
super_start() {
(( service_running )) && return 3
2017-11-09 04:19:26 +03:00
rm -f "$service_stopped_flag"
2016-11-13 14:47:20 +03:00
[[ -f "${service_command[0]}" ]] || return 9
depend "${service_depends[@]}" || return 7
depend_ready "${service_depends_ready[@]}" || return 7
2016-11-13 17:33:58 +03:00
mktmpfiles || return 13
2016-11-13 11:11:47 +03:00
if (( service_managed )); then
if (( service_respawn )); then
svc respawn "${service_command[@]}" &>"$service_logfile" &
else
svc "${service_command[@]}" &>"$service_logfile" &
fi
2016-11-13 16:31:17 +03:00
if timer "$service_ready_timeout" ready; then
printf '1' > "$service_ready_flag"
else
return 5
fi
elif (( service_oneshot )); then
2016-11-17 12:26:23 +03:00
"${service_command[@]}" &>"$service_logfile"; res=$?
2016-11-13 16:31:17 +03:00
(( $res )) && return "$res"
printf '1' > "$service_enabled_flag"
2016-11-13 14:47:20 +03:00
else
2016-11-13 16:31:17 +03:00
exec "${service_command[@]}" &
2016-11-13 14:47:20 +03:00
fi
2016-11-13 11:11:47 +03:00
return 0
}
2016-11-13 14:47:20 +03:00
# A separate function for oneshot services
super_oneshot() {
(( service_enabled )) && return 3
}
2016-11-13 11:11:47 +03:00
## Reload the service
## Usually just sends HUP
super_reload() {
(( service_running )) || return 3
2016-11-13 16:31:17 +03:00
if (( service_managed )); then
kill -n 1 "$service_pid"
else
kill -n "$service_reload_signal" "$service_pid"
fi
2016-11-13 11:11:47 +03:00
}
## Stop the service
## Returns:
## 3: Service is not running.
super_stop() {
2016-11-13 16:31:17 +03:00
if (( service_oneshot )); then
(( service_enabled )) || return 3
2016-11-13 11:11:47 +03:00
2016-11-13 16:31:17 +03:00
rm -f "$service_enabled_flag"
2016-11-13 11:11:47 +03:00
2016-11-13 16:31:17 +03:00
return 0
else
(( service_running )) || return 3
nullexec kill -n "$service_stop_signal" "$service_pid" || return 1
pid_wait "$service_pid" || return 5
2017-11-09 04:19:26 +03:00
> "$service_stopped_flag"
2016-11-13 16:31:17 +03:00
return 0
fi
2016-11-13 11:11:47 +03:00
}
2017-03-11 17:36:07 +03:00
info() {
2017-03-11 17:57:16 +03:00
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[*]}"
2017-03-11 17:57:16 +03:00
"Respawn" "${service_respawn:-false}"
2017-12-10 19:48:22 +03:00
"Config path" "${service_config}"
2017-03-11 17:57:16 +03:00
)
[[ "$_status" == 'yes' ]] && {
_info_items+=(
"PIDfile" "${service_pidfile:-none}"
"PID" "${service_pid:-none}"
)
}
printf "%12s: %s\n" "${_info_items[@]}"
2017-03-11 17:36:07 +03:00
}
2016-11-17 13:11:11 +03:00
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]}"
}
2016-11-13 11:11:47 +03:00
# Overloadable functions
start() { super_start; }
stop() { super_stop; }
2016-11-13 14:47:20 +03:00
reload() { super_reload; }
2016-11-13 11:11:47 +03:00
restart() {
2017-12-14 07:07:48 +03:00
"$_self" "$service_name" stop
"$_self" "$service_name" start
2016-11-13 11:11:47 +03:00
}
logs() { ${PAGER:-less} "$service_logfile"; }
2016-11-13 14:47:20 +03:00
## Status is a bit of a special case. It's talkative.
status() {
(( service_running )) && return 0
2016-11-13 16:31:17 +03:00
(( service_enabled )) && return 0
2017-11-09 04:19:26 +03:00
(( service_stopped )) && return 7
2016-11-13 14:47:20 +03:00
return 1
}
## For use in scripts
qstatus() { nullexec status; }
## By default there is no ready check
ready() { :; }
2016-11-13 11:11:47 +03:00
# Code
main() {
2017-12-14 07:07:48 +03:00
# Figure out our full path
case "$0" in
(/*) _self=$0;;
(*) _self="$PWD/$0";;
esac
2016-11-13 17:33:58 +03:00
# Needs to be global
declare -g service_pid
2016-11-13 11:11:47 +03:00
# Let's set some defaults
service_managed=1
2017-12-08 15:16:03 +03:00
usrdir='/usr/share/ssm'
2016-11-13 11:11:47 +03:00
if (( $UID )); then
# XDG stuff
default XDG_CONFIG_HOME "$HOME/.config"
default XDG_RUNTIME_DIR "/run/user/$UID"
2017-12-10 19:27:51 +03:00
service_path=( "$XDG_CONFIG_HOME/ssm/services" )
2017-12-08 16:39:05 +03:00
cfg_path=( "$XDG_CONFIG_HOME/ssm" )
2017-12-10 19:42:25 +03:00
# 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
2016-11-13 19:20:10 +03:00
rundir="$XDG_RUNTIME_DIR/ssm"
logdir="$HOME/log/ssm"
2016-11-13 11:11:47 +03:00
else
2016-11-13 19:20:10 +03:00
rundir='/run/ssm'
logdir='/var/log/ssm'
2016-11-13 11:11:47 +03:00
fi
2017-12-10 19:42:25 +03:00
# 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
2017-12-08 16:39:05 +03:00
# Common service path
2017-12-10 19:27:51 +03:00
service_path+=( '/etc/ssm/services' "$rundir/services" "$usrdir/services" )
2017-12-08 16:39:05 +03:00
# Common config path
2017-12-10 20:13:51 +03:00
cfg_path+=( '/etc/ssm' )
2017-03-12 04:30:36 +03:00
2016-11-13 11:11:47 +03:00
# Load custom functions
2017-12-08 16:49:34 +03:00
for (( idx=${#cfg_path[@]}-1; idx>=0; idx-- )); do
cfg_dir="${cfg_path[idx]}"
for f in "$cfg_dir/functions"/*; do
2017-12-08 16:39:05 +03:00
source "$f" || die 9 "Failed to source functions from $f"
done
2016-11-13 11:11:47 +03:00
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
2016-11-14 06:31:53 +03:00
# If $1 is a full path, source it.
2017-03-12 04:30:36 +03:00
# If not, search for it in the service path.
2016-11-14 06:31:53 +03:00
if [[ $1 == /* ]]; then
service_config=$1
else
2017-03-12 04:30:36 +03:00
for i in "${service_path[@]}"; do
2017-12-08 16:39:05 +03:00
[[ -f "$i/$1" ]] && {
service_config="$i/$1"
break
}
2017-03-12 04:30:36 +03:00
done
2016-11-14 06:31:53 +03:00
fi
# Die if there is no such file
2017-03-12 04:30:36 +03:00
[[ "$service_config" ]] || die 19 "Service not found: $1"
2016-11-14 06:31:53 +03:00
# Service name is the basename
service_name="${1##*/}"
2016-11-13 11:11:47 +03:00
# Semi-hardcoded stuff
svc_pidfile="$rundir/$service_name.pid"
# Get the service defaults
2017-12-08 16:39:05 +03:00
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
2016-11-13 11:11:47 +03:00
# Get the service config
2017-12-08 16:39:05 +03:00
source -- "$service_config" "${@:3}" || die 7 "Failed to read the service config: $service_config"
2016-11-13 11:11:47 +03:00
# Legacy
[[ "$service_args" ]] && service_command=( "${service_command[@]}" "${service_args[@]}" )
[[ "$service_respawn" == 'true' ]] && service_respawn=1
2017-03-12 04:30:36 +03:00
[[ "$service_type" == 'oneshot' ]] && service_oneshot=1
(( service_oneshot )) && service_managed=0
2016-11-13 11:11:47 +03:00
[[ "$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
2017-12-08 14:39:50 +03:00
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"
2017-12-14 07:07:48 +03:00
default service_workdir '/'
2017-12-08 14:39:50 +03:00
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
2016-11-13 17:33:58 +03:00
2016-11-13 11:11:47 +03:00
# 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
2016-11-13 16:31:17 +03:00
# Maybe the service is enabled?
if [[ -f "$service_enabled_flag" ]]; then
# Yay, it is!
service_enabled=1
fi
2017-11-09 04:19:26 +03:00
# Let's see if the service was deliberately stopped
if [[ -f "$service_stopped_flag" ]]; then
# Ooh, it was.
service_stopped=1
fi
2016-11-13 11:11:47 +03:00
# Check if action is even defined
is_function "$2" || die 17 "Function $2 is not defined for $service_name."
2017-11-09 04:19:26 +03:00
# cd into the workdir, if defined.
[[ "$service_workdir" ]] && {
cd "$service_workdir" || die $?
}
2016-11-13 11:11:47 +03:00
# Run pre_$action function
if is_function "pre_$2"; then
"pre_$2" || {
2016-11-13 14:47:20 +03:00
printf 'pre_%s failed!\n' "$2"
die 13
2016-11-13 11:11:47 +03:00
}
fi
# Run the function
2017-07-13 15:37:53 +03:00
"$2"; res=$?
2016-11-13 11:11:47 +03:00
case "$2" in
stop)
2016-11-17 13:11:11 +03:00
result "$res" \
0 "Stopped $service_name" \
3 "$service_name is not running" \
5 "Operation timed out"
2016-11-13 11:11:47 +03:00
;;
start)
2016-11-17 13:11:11 +03:00
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"
2016-11-13 11:11:47 +03:00
;;
2016-11-13 14:47:20 +03:00
reload)
2016-11-17 13:11:11 +03:00
result "$res" \
0 "Reloaded $service_name"
2016-11-13 14:47:20 +03:00
;;
status)
2016-11-17 13:11:11 +03:00
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" \
2017-11-09 04:19:26 +03:00
1 "$service_name is not running" \
7 "$service_name was stopped"
2016-11-17 13:11:11 +03:00
fi
2016-11-13 14:47:20 +03:00
;;
2016-11-13 11:11:47 +03:00
esac
2016-11-13 14:47:20 +03:00
(( res )) && return "$res"
2016-11-13 11:11:47 +03:00
# Run post_$action function
if is_function "post_$2"; then
"post_$2" || {
2016-11-13 14:47:20 +03:00
printf 'post_%s failed!\n' "$2"
die 15
2016-11-13 11:11:47 +03:00
}
fi
}
main "$@"