528 lines
10 KiB
Bash
Executable File
528 lines
10 KiB
Bash
Executable File
#!/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 ! "$0" "$s" qstatus; then
|
|
nullexec "$0" "$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
|
|
"$0" "$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}"
|
|
)
|
|
|
|
[[ "$_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() {
|
|
"$0" "$service_name" stop
|
|
"$0" "$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() {
|
|
# 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" )
|
|
|
|
rundir="$XDG_RUNTIME_DIR/ssm"
|
|
logdir="$HOME/log/ssm"
|
|
else
|
|
rundir='/run/ssm'
|
|
logdir='/var/log/ssm'
|
|
fi
|
|
|
|
# Common service path
|
|
service_path+=( '/etc/ssm/init.d' "$rundir/services" "$usrdir/services" )
|
|
|
|
# Common config path
|
|
cfg_path+=( '/etc/ssm/conf.d' )
|
|
|
|
# Load custom functions
|
|
for p in "${cfg_path[@]}"; do
|
|
for f in "$p/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_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 "$@"
|