1022 lines
24 KiB
Bash
Executable File
1022 lines
24 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
shopt -s nullglob
|
|
|
|
# Utility functions
|
|
is_function() [[ "$(type -t "$1" 2>/dev/null)" == 'function' ]]
|
|
readonly -f is_function
|
|
|
|
command_not_found_handle() {
|
|
declare f=$1; shift; declare argv=( "$@" )
|
|
|
|
# Skip the special name 'source'.
|
|
# Don't call your functions 'source'.
|
|
declare i
|
|
for i in "${FUNCNAME[@]:1}"; do
|
|
[[ $i == 'source' ]] && continue
|
|
f="${i}${f}"
|
|
break
|
|
done
|
|
|
|
is_function "$f" || {
|
|
printf '%s: line %s: %s: command not found\n' "$0" "${BASH_LINENO[0]}" "$f" >&2
|
|
return 127
|
|
}
|
|
|
|
"$f" "${argv[@]}"
|
|
}; readonly command_not_found_handle;
|
|
|
|
usage() {
|
|
cat <<- EOF
|
|
Usage: ssm <service> <function>
|
|
EOF
|
|
}; readonly usage;
|
|
|
|
var() {
|
|
declare var_function=$1; shift
|
|
declare var_name
|
|
|
|
# This enforces bash's grammar against things like
|
|
# var 'cat /etc/shadow; foo' ...
|
|
[[ $var_function =~ ^[a-zA-Z_][a-zA-Z0-9_]+?$ ]] || {
|
|
die 73 "On line $LINENO, in $FUNCNAME: Invalid identifier: '$var_function'"
|
|
}
|
|
|
|
if [[ "$1" == '-v' ]]; then
|
|
var_name=$2
|
|
shift 2
|
|
else
|
|
var_name=$var_function
|
|
fi
|
|
|
|
if ! is_function "$var_function"; then
|
|
eval "
|
|
${var_function}() {
|
|
declare mode=set
|
|
declare -n _var=\"${var_name}\"
|
|
|
|
if (( \$# )); then
|
|
case \"\$1\" in
|
|
('=') mode=set;;
|
|
('+=') mode=append;;
|
|
('_=') mode=prepend;;
|
|
(':=') mode=default;;
|
|
|
|
('==') mode=compare;;
|
|
('=~') mode=regex;;
|
|
|
|
('is') mode=\"is_\$2\";;
|
|
|
|
('u') mode=includes;;
|
|
|
|
(*) die 71 \"Syntax error in ${var_function}!\";;
|
|
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
|
|
;;
|
|
|
|
(includes)
|
|
for i in \"\${_var[@]}\"; do
|
|
[[ \"\$i\" == \"\$*\" ]] && return 0
|
|
done
|
|
|
|
return 1
|
|
;;
|
|
|
|
(is_fs_object) [[ -e \"\$_var\" ]];;
|
|
(is_file) [[ -f \"\$_var\" ]];;
|
|
(is_dir|is_directory) [[ -d \"\$_var\" ]];;
|
|
(is_empty) [[ -z \"\${_var[*]}\" ]];;
|
|
|
|
(*) die 71 \"Syntax error in ${var_function}!\";;
|
|
esac
|
|
}; readonly -f \"${var_function}\"
|
|
|
|
${var_function}++() {
|
|
declare -n _var=\"${var_name}\"
|
|
(( _var++ ))
|
|
}
|
|
|
|
${var_function}--() {
|
|
declare -n _var=\"${var_name}\"
|
|
(( _var-- ))
|
|
}
|
|
"
|
|
fi
|
|
|
|
if (( $# )); then
|
|
case "$1" in
|
|
('='|'=='|'=~'|'+='|'_='|':=')
|
|
"$var_function" "$@"
|
|
;;
|
|
|
|
(*)
|
|
for v in "$@"; do
|
|
var "$v"
|
|
done
|
|
;;
|
|
esac
|
|
fi
|
|
}; readonly -f var
|
|
|
|
## Die. Why not?
|
|
die() {
|
|
declare code=${1:-0}
|
|
|
|
[[ "$2" ]] && printf '%s\n' "$2" >&2
|
|
exit "$code"
|
|
}; readonly -f die
|
|
|
|
if_service_action() {
|
|
for f in "service::$1" "$1"; do
|
|
is_function "$f" && return 0
|
|
done
|
|
|
|
return 1
|
|
}; readonly -f if_service_action
|
|
|
|
run_service_action() {
|
|
for f in "service::$1" "$1"; do
|
|
is_function "$f" && {
|
|
"$f"; return $?
|
|
}
|
|
done
|
|
|
|
return 0
|
|
}; readonly -f run_service_action
|
|
|
|
spawn() {
|
|
if [[ $service_logfile_out == "$service_logfile_err" ]]; then
|
|
exec "$@" >"$service_logfile_out" 2>&1
|
|
else
|
|
exec "$@" >"$service_logfile_out" 2>"$service_logfile_err"
|
|
fi
|
|
}; readonly spawn;
|
|
|
|
cgroup_get_procs() {
|
|
if service_cgroup_path is dir; then
|
|
mapfile -t service_cgroup_procs < "$cgroup_home/$service_cgroup_name/cgroup.procs"
|
|
fi
|
|
}; readonly -f cgroup_get_procs
|
|
|
|
## Run the command and wait for it to die
|
|
svc() {
|
|
declare job_pid job_exit job_success last_respawn fail_counter date counter p
|
|
var job_pid job_exit job_success last_respawn fail_counter date counter p
|
|
|
|
svc::cleanup() {
|
|
#nullexec kill -n "$service_stop_signal" "$job_pid"
|
|
nullexec "${service_stop_command[@]}"
|
|
|
|
anywait "$job_pid" "$service_stop_timeout"
|
|
|
|
# Cgroup stuff
|
|
if cgroups; then
|
|
if service_cgroup_cleanup; then
|
|
cgroup_get_procs
|
|
|
|
service_cgroup_procs is empty || {
|
|
for p in "${service_cgroup_procs[@]}"; do
|
|
p == "$BASHPID" || {
|
|
nullexec kill -n "$service_cgroup_kill_signal" "$p"
|
|
anywait "$p" "$service_stop_timeout" &
|
|
}
|
|
done
|
|
|
|
wait || die $?
|
|
}
|
|
fi
|
|
fi
|
|
|
|
run_service_action 'cleanup'
|
|
|
|
rm -f "$svc_pidfile" "$service_pidfile" "$service_ready_flag"
|
|
|
|
die "$job_exit"
|
|
}; trap 'svc::cleanup' TERM
|
|
|
|
printf '%s' $BASHPID > "$svc_pidfile"
|
|
|
|
# Cgroups
|
|
if cgroups; then
|
|
mkdir -p "$cgroup_home/$service_cgroup_name"
|
|
echo "$BASHPID" > "$cgroup_home/$service_cgroup_name/cgroup.procs"
|
|
fi
|
|
|
|
while true; do
|
|
job_success = 0 # Needs to be reset
|
|
|
|
# Remove stale pidfiles some services may leave behind
|
|
# If a pidfile exists at this point in the code, it should be stale.
|
|
if service_pidfile_remove_stale; then
|
|
if ! svc_pidfile == "$service_pidfile"; then
|
|
service_pidfile is file && rm -f "$service_pidfile"
|
|
fi
|
|
fi
|
|
|
|
# Some setup might be required on each loop
|
|
run_service_action 'setup'
|
|
|
|
# Spawn the process and record the PID
|
|
spawn "$@" & job_pid = "$!"
|
|
|
|
# Wait for the process to exit and record the exit code
|
|
# This depends on a few things
|
|
if service_own_pidfile; then
|
|
# We need to wait for the service to write down its pidfile
|
|
until service_pidfile is file; do
|
|
(( counter >= service_pidfile_timeout*10 )) && {
|
|
printf '127' > "$service_exit_file"
|
|
break
|
|
}
|
|
counter++
|
|
sleep 0.1
|
|
done
|
|
|
|
read -r job_pid < "$service_pidfile"
|
|
|
|
# We consider any termination of a service with its own pidfile
|
|
# to be a failure
|
|
anywait "$job_pid"; job_exit=127
|
|
else
|
|
printf '%s' "$job_pid" > "$service_pidfile"
|
|
|
|
wait "$job_pid"; job_exit=$?
|
|
fi
|
|
|
|
# One service failure, two service failures...
|
|
if service_success_exit u "$job_exit"; then
|
|
job_success = 1
|
|
(( fail_counter )) && fail_counter--
|
|
else
|
|
job_success = 0
|
|
fail_counter++
|
|
fi
|
|
|
|
# Record the exit code
|
|
printf '%s' "$job_exit" > "$service_exit_file"
|
|
|
|
# Back off if the service exits too much AND too quickly.
|
|
if ! service_respawn_force; then
|
|
if (( fail_counter >= 3 )); then
|
|
printf -v date '%(%s)T'
|
|
|
|
(( (date - last_respawn) <= 5 )) && break
|
|
fi
|
|
fi
|
|
|
|
# Respawn, if necessary
|
|
service_respawn_flag || break
|
|
case $service_respawn in
|
|
(on-success) job_success || break;;
|
|
(on-failure) job_success && break;;
|
|
esac
|
|
|
|
# Record the time every time we restart the loop
|
|
printf -v last_respawn '%(%s)T'
|
|
done
|
|
|
|
svc::cleanup
|
|
}; readonly -f svc
|
|
|
|
## Run a command with its output discarded
|
|
nullexec() { eval "$@" &>/dev/null; }
|
|
readonly -f nullexec
|
|
|
|
## Wait for a pid, indefinitely
|
|
anywait() {
|
|
declare counter timeout
|
|
var counter = 0
|
|
var timeout = "$2"
|
|
|
|
while nullexec kill -0 "$1"; do
|
|
timeout && {
|
|
(( counter >= timeout )) && return 1
|
|
counter++
|
|
}
|
|
|
|
sleep 0.1
|
|
done
|
|
|
|
return 0
|
|
}; readonly -f anywait
|
|
|
|
## 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
|
|
|
|
## Set a service's ready flag.
|
|
set_ready() { printf '1' > "$service_ready_flag"; }
|
|
readonly -f set_ready
|
|
|
|
## Is a service ready?
|
|
is_ready() { service_ready_flag is file; }
|
|
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]}" >&2
|
|
}; readonly -f result
|
|
|
|
# Overloadable functions
|
|
## Start the service, write down the svc pid
|
|
start() {
|
|
service_running && return 3
|
|
service_enabled && return 19
|
|
service_command is file || return 9
|
|
|
|
# Preform cgroup checks
|
|
if cgroups; then
|
|
if service_cgroup_exclusive; then
|
|
service_cgroup_procs is empty || return 15
|
|
fi
|
|
fi
|
|
|
|
depend "${service_depends[@]}" || return 7
|
|
depend_ready "${service_depends_ready[@]}" || return 7
|
|
|
|
rm -f "$service_stopped_flag"
|
|
|
|
mktmpfiles || return 13
|
|
|
|
svc "${service_command[@]}" & job=$!
|
|
|
|
if service_oneshot; then
|
|
wait "$job"; res=$?
|
|
|
|
(( res )) && {
|
|
printf '%s' "$res" > "$service_exit_file"
|
|
return 17
|
|
}
|
|
fi
|
|
|
|
if timer "$service_ready_timeout" run_service_action 'ready'; then
|
|
set_ready
|
|
else
|
|
return 5
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
## Reload the service
|
|
## Usually just sends HUP
|
|
reload() {
|
|
service_running || return 3
|
|
nullexec kill -n "$service_reload_signal" "$service_pid"
|
|
}
|
|
|
|
## Stop the service
|
|
## Returns:
|
|
## 3: Service is not running.
|
|
stop() {
|
|
if service_oneshot; then
|
|
return 7
|
|
else
|
|
service_running || return 3
|
|
|
|
kill -n 15 "$svc_pid" || return 1
|
|
|
|
anywait "$svc_pid" "$service_stop_timeout" || return 5
|
|
> "$service_stopped_flag"
|
|
|
|
# Cgroup stuff
|
|
if cgroups; then
|
|
if service_cgroup_kill; then
|
|
service_cgroup_wait = 1
|
|
|
|
for p in "${service_cgroup_procs[@]}"; do
|
|
nullexec kill -n "$service_cgroup_kill_signal" "$p"
|
|
done
|
|
fi
|
|
|
|
if service_cgroup_wait; then
|
|
for p in "${service_cgroup_procs[@]}"; do
|
|
anywait "$p" "$service_stop_timeout" &
|
|
done
|
|
|
|
wait || return 5
|
|
fi
|
|
fi
|
|
|
|
return 0
|
|
fi
|
|
}
|
|
|
|
info.item() { printf "%16s: %s\n" "$1" "$2"; }
|
|
info() {
|
|
declare -a _info_items
|
|
declare _status_code _status _show_pid
|
|
var _info_items _status_code _status _show_pid
|
|
|
|
.item Name "$service_name"
|
|
|
|
status; _status_code = "$?"
|
|
if service_oneshot; then
|
|
.item Oneshot 'yes'
|
|
|
|
case $_status_code in
|
|
(0) _status = 'success';;
|
|
(1) _status = 'not enabled';;
|
|
(9) _status = "failed ($service_exit_last)";;
|
|
(*) _status = 'unknown';;
|
|
esac
|
|
else
|
|
.item Restart "$service_respawn"
|
|
|
|
case $_status_code in
|
|
(0) _status = 'running';;
|
|
(1) _status = 'down';;
|
|
(7) _status = 'stopped';;
|
|
(9) _status = "failed ($service_exit_last)";;
|
|
(11) _status = "exited ($service_exit_last)";;
|
|
(*) _status = 'unknown';;
|
|
esac
|
|
fi
|
|
|
|
.item Status "$_status"
|
|
|
|
.item Exec "${service_command[*]}"
|
|
.item Config "$service_config"
|
|
.item Workdir "$service_workdir"
|
|
|
|
.item 'Output log' "$service_logfile_out"
|
|
service_logfile_out == "$service_logfile_err" || \
|
|
.item 'Error log' "$service_logfile_err"
|
|
|
|
if service_running; then
|
|
.item PID "$service_pid"
|
|
.item PIDFile "$service_pidfile"
|
|
|
|
cgroups && {
|
|
.item Cgroup "$cgroup_home/$service_cgroup_name"
|
|
.item 'Cgroup procs' "${#service_cgroup_procs[@]}"
|
|
}
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
## Restart just calls the script twice by default
|
|
restart() {
|
|
"$_self" "$service_name" stop
|
|
"$_self" "$service_name" start
|
|
}
|
|
|
|
edit() { $EDITOR "$service_config"; }
|
|
logs() {
|
|
if service_logfile_out == "$service_logfile_err"; then
|
|
$PAGER "$service_logfile_out"
|
|
else
|
|
printf '%s\n' "$service_logfile_out" "$service_logfile_err"
|
|
fi
|
|
}
|
|
|
|
## Status is a bit of a special case. It's talkative.
|
|
status() {
|
|
service_running && return 0
|
|
service_enabled && return 0
|
|
|
|
service_stopped && return 7
|
|
|
|
service_failed && return 9
|
|
service_exit_last is empty || return 11
|
|
|
|
return 1
|
|
}
|
|
|
|
## For use in scripts
|
|
qstatus() { nullexec status; }
|
|
|
|
## By default there is no ready check
|
|
ready() { :; }
|
|
|
|
## Reset failes
|
|
reset-exit() { rm -f "$service_exit_file"; }
|
|
|
|
# Main code
|
|
## Empty declarations
|
|
var service_pid \
|
|
service_pidfile \
|
|
service_type \
|
|
service_depends \
|
|
service_depends_ready \
|
|
service_command \
|
|
service_config \
|
|
service_path \
|
|
service_name \
|
|
service_args \
|
|
service_logfile_out \
|
|
service_logfile_err \
|
|
service_ready_flag \
|
|
service_stopped_flag \
|
|
service_exit_file \
|
|
service_exit_last \
|
|
service_cgroup_name \
|
|
service_cgroup_procs \
|
|
service_cgroup_path \
|
|
service_signals_passthru \
|
|
service_config_current \
|
|
cgroup_home \
|
|
failed_deps \
|
|
svc_pidfile \
|
|
svc_pid \
|
|
cfg_path \
|
|
cfg_file \
|
|
cfg_dir \
|
|
rundir \
|
|
logdir \
|
|
action \
|
|
_self \
|
|
res
|
|
|
|
## Internal defaults
|
|
var flag_list_services = 0
|
|
var flag_edit_service = 0
|
|
var flag_reset_exit = 0
|
|
var flag_reread_service = 0
|
|
var flag_forget_service = 0
|
|
var flag_load_service = 0
|
|
|
|
## 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
|
|
# These are meaningful to reconfigure.
|
|
var service_respawn = 'no' # Respawn the service if it exits
|
|
var service_workdir = '/'
|
|
var service_stop_timeout = 30
|
|
var service_stop_command = kill -n "\$service_stop_signal" "\$job_pid"
|
|
var service_ready_timeout = 15
|
|
var service_signals = 1 10 12
|
|
var service_reload_signal = 1
|
|
var service_stop_signal = 15
|
|
var service_cgroup_exclusive = 0 # Refuse to start the service if its cgroup is not empty
|
|
var service_cgroup_wait = 0 # Wait on all the members of the cgroup to exit when stopping the service.
|
|
var service_cgroup_strict = 1 # Enable checking if the main service PID is in the correct cgroup before doing anythin with the service
|
|
var service_cgroup_kill = 0 # Kill the entire cgroup when stopping the service.
|
|
var service_cgroup_kill_signal = 15 # The signal to send to the stray cgroup members.
|
|
var service_cgroup_cleanup = 0 # Clean up the cgroup when the main PID exits. Uses service_cgroup_kill_signal.
|
|
var service_success_exit = 0 # Array, takes exit codes that are to be treated as successful termination.
|
|
var service_pidfile_timeout = 15 # How long to wait for unmanaged services to create their pidfiles.
|
|
var service_pidfile_remove_stale = 1 # Remove stale pidfiles from unmanaged services.
|
|
var service_remember = 1 # Copy the config into ssm's runtime dir.
|
|
|
|
# Global config
|
|
var cgroups = 0 # Enable cgroup-related functions
|
|
var usrdir = '/usr/share/ssm'
|
|
|
|
# These are not
|
|
var service_own_pidfile = 0
|
|
var service_oneshot = 0
|
|
var service_running = 0
|
|
var service_enabled = 0
|
|
var service_stopped = 0
|
|
var service_failed = 0
|
|
var service_nologs = 0
|
|
var service_respawn_flag = 0
|
|
var service_respawn_force = 0
|
|
|
|
# These depend on who we are
|
|
if (( $UID )); then
|
|
rundir = "$XDG_RUNTIME_DIR/ssm"
|
|
logdir = "$HOME/log/ssm"
|
|
cgroup_home = "/sys/fs/cgroup/user/$UID/ssm"
|
|
cfg_file = "$XDG_CONFIG_HOME/ssm/ssm.conf"
|
|
else
|
|
rundir = '/run/ssm'
|
|
logdir = '/var/log/ssm'
|
|
cgroup_home = "/sys/fs/cgroup/ssm"
|
|
cfg_file = '/etc/ssm/ssm.conf'
|
|
fi
|
|
|
|
## Figure out our full path
|
|
case "$0" in
|
|
(/*) _self = "$0";;
|
|
(*) _self = "$PWD/$0";;
|
|
esac
|
|
|
|
# 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
|
|
|
|
# Source the config
|
|
if cfg_file is file; then
|
|
source "$cfg_file" || die 37 "Failed to load config: $cfg_file"
|
|
fi
|
|
|
|
# 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]}"
|
|
|
|
for f in "$cfg_dir/functions"/*; do
|
|
source "$f" || die 9 "Failed to source functions from $f"
|
|
done
|
|
done
|
|
|
|
# Parse arguments
|
|
while (( $# )); do
|
|
case $1 in
|
|
(-h|--help) # Show help
|
|
usage; exit 0;;
|
|
|
|
(-L|--list-services) # List all services
|
|
flag_list_services = 1;;
|
|
|
|
(--reset-exit) # Reset last exit status
|
|
flag_reset_exit = 1;;
|
|
|
|
(-e|--edit) # Edit the service file.
|
|
flag_edit_service = 1;;
|
|
|
|
(-r|--reread) # Reload the service file.
|
|
flag_reread_service = 1;;
|
|
|
|
(-f|--forget) # Unload a service.
|
|
flag_forget_service = 1;;
|
|
|
|
(-l|--load) # Load a service.
|
|
flag_load_service = 1;;
|
|
|
|
(-i|--info)
|
|
action = 'info';;
|
|
|
|
(--) shift; break;;
|
|
(-*) printf 'Unknown key: %s\n' "$1" >&2; exit 1;;
|
|
(*) break;;
|
|
esac
|
|
|
|
shift
|
|
done
|
|
|
|
# Now create the needed runtime stuff
|
|
for d in "$rundir" "$rundir/current" "$logdir"; do
|
|
mkdir -p "$d" || die 3 "Failed to create runtime dir: $d"
|
|
done
|
|
|
|
# Common service path
|
|
service_path += "$XDG_CONFIG_HOME/ssm/services" '/etc/ssm/services' "$rundir/services" "$usrdir/services"
|
|
|
|
# Special actions
|
|
if flag_list_services; then
|
|
var known_services
|
|
for i in "$rundir"/current/*; do
|
|
i_name="${i##*/}"
|
|
known_services u "$i_name" || known_services += "$i_name"
|
|
done
|
|
|
|
for s in "${known_services[@]}"; do
|
|
printf '%s: ' "$s"
|
|
ssm "$s" status
|
|
done
|
|
|
|
die 0
|
|
fi
|
|
|
|
# This script requires at least one argument
|
|
(( $# >= 1 )) || { usage; exit 2; }
|
|
|
|
# Unless otherwise specified
|
|
service_name = "$1"; shift
|
|
|
|
# Find the current loaded service and remove it if necessary
|
|
service_config_current = "$rundir/current/$service_name"
|
|
if flag_reread_service; then
|
|
rm -vf "$service_config_current" >&2 || die "$?" "Abort!"
|
|
fi
|
|
|
|
# Find the service
|
|
if [[ $service_name == /* ]]; then
|
|
service_config = "$service_name"
|
|
service_name = "${service_name##*/}"
|
|
else
|
|
for i in "$service_config_current" "${service_path[@]/%//$service_name}"; do
|
|
[[ -f "$i" ]] && {
|
|
service_config = "$i"
|
|
break
|
|
}
|
|
done
|
|
fi
|
|
|
|
# We really don't want this overriden
|
|
readonly service_name
|
|
|
|
# Unload the service and leave if asked.
|
|
if flag_forget_service; then
|
|
if service_config_current is file; then
|
|
rm -f "$service_config_current" || die "$?" "Abort!"
|
|
die 0 "Forgot service: $service_name"
|
|
else
|
|
die 90 "Service not currently known: $service_name"
|
|
fi
|
|
fi
|
|
|
|
# Die if there is no service config file
|
|
service_config || die 19 "Service not found: $service_name"
|
|
|
|
# Edit the service config
|
|
flag_edit_service && { edit; die $?; }
|
|
|
|
# These depend on the service_name and make little sense to reconfigure.
|
|
service_ready_flag := "$rundir/$service_name.ready"
|
|
service_stopped_flag := "$rundir/$service_name.stopped"
|
|
service_exit_file := "$rundir/$service_name.exit"
|
|
service_cgroup_name := "$service_name"
|
|
service_cgroup_path := "$cgroup_home/$service_name"
|
|
|
|
# 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
|
|
|
|
# Reset the exit status
|
|
flag_reset_exit && { reset-exit; die $?; }
|
|
|
|
# Get the service config
|
|
source -- "$service_config" "${@:3}" || die 7 "Failed to read the service config: $service_config"
|
|
|
|
# “Load” the service into memory
|
|
if ! service_config == "$service_config_current" && service_remember; then
|
|
cat "$service_config" > "$rundir/current/$service_name"
|
|
printf 'Loaded %s from: %s\n' "$service_name" "$service_config" >&2
|
|
service_config = "$rundir/current/$service_name"
|
|
fi
|
|
|
|
# Die if we only needed to load a service
|
|
flag_load_service && die 0
|
|
|
|
# Legacy
|
|
service_args && service_command += "${service_args[@]}"
|
|
service_type == 'oneshot' && service_oneshot = 1
|
|
service_respawn == 1 && service_respawn = always
|
|
|
|
# Set respawn flag
|
|
if ! service_respawn == 'no'; then
|
|
case $service_respawn in
|
|
(on-failure|on-success|always) service_respawn_flag = 1;;
|
|
(*) die 88 "Wrong value for service_respawn";;
|
|
esac
|
|
fi
|
|
|
|
if service_respawn_flag && service_oneshot; then
|
|
die 89 'Cowardly refusing to respawn a oneshot service'
|
|
fi
|
|
|
|
# Catch services with their own pidfile, set the appropriate flag.
|
|
service_pidfile && service_own_pidfile = 1
|
|
|
|
# Semi-hardcoded stuff
|
|
svc_pidfile = "$rundir/$service_name.svc_pid"
|
|
|
|
# Service-level defaults
|
|
service_pidfile := "$rundir/$service_name.pid"
|
|
service_logfile_out := "$logdir/${service_name}.log"
|
|
service_logfile_err := "$service_logfile_out"
|
|
service_success_exit := 0
|
|
|
|
# A shortcut for disabling logging
|
|
if service_nologs; then
|
|
service_logfile_out = '/dev/null'
|
|
service_logfile_err = '/dev/null'
|
|
fi
|
|
|
|
# Get the last recorded mainpid
|
|
if service_pidfile is file; then
|
|
read -r service_pid < "$service_pidfile"
|
|
fi
|
|
|
|
# Let's see if there's an svc running
|
|
if svc_pidfile is file; then
|
|
read -r svc_pid < "$svc_pidfile"
|
|
|
|
# Let's see if it's running
|
|
if nullexec kill -0 "$svc_pid"; then
|
|
service_running = 1
|
|
|
|
# If it's running, we know its PID probably:
|
|
if service_pid; then
|
|
if ! nullexec kill -0 "$service_pid"; then
|
|
printf 'WARNING: The recorded service main PID (%s) is not running.\n' "$service_pid" >&2
|
|
fi
|
|
else
|
|
printf 'WARNING: No service pidfile found; service PID unknown.\n' "$service_pidfile" >&2
|
|
fi
|
|
else
|
|
if nullexec kill -0 "$service_pid"; then
|
|
die 75 "ERROR: No svc active for $service_name, but its last recorded PID ($service_pidfile) is currenlty running: ${service_pid}."
|
|
fi
|
|
|
|
# Remove the stale svc pidfile
|
|
printf 'WARNING: Removing a stale svc pidfile: %s\n' "$svc_pidfile" >&2
|
|
rm -f "$svc_pidfile"
|
|
fi
|
|
fi
|
|
|
|
# Let's see if the service was deliberately stopped
|
|
if service_stopped_flag is file; then
|
|
# Ooh, it was.
|
|
service_stopped = 1
|
|
fi
|
|
|
|
# Check if the service has failed
|
|
if service_exit_file is file; then
|
|
read -r service_exit_last < "$service_exit_file"
|
|
|
|
if service_success_exit u "$service_exit_last"; then
|
|
service_oneshot && service_enabled = 1
|
|
else
|
|
# :(
|
|
service_failed = 1
|
|
fi
|
|
fi
|
|
|
|
# Check cgroups, if enabled
|
|
if cgroups; then
|
|
cgroup_get_procs
|
|
|
|
# If there's a service PID, check if it's in the service's cgroup
|
|
if service_cgroup_strict; then
|
|
if service_running; then
|
|
service_cgroup_procs u "$service_pid" || die 29 "Recorded service PID is not in the service's cgroup, bailing!"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Action!
|
|
[[ "$1" ]] && action = "$1"
|
|
action || {
|
|
flag_reread_service && die 0 # Not a mistake if the service was to be re-read
|
|
usage; die 2; }
|
|
|
|
# Do we have such a function?
|
|
if_service_action "$action" || die 17 "Function $action is not defined for $service_name."
|
|
|
|
# cd into the workdir, if defined.
|
|
service_workdir && {
|
|
cd "$service_workdir" || die $?
|
|
}
|
|
|
|
# Run pre_$action function
|
|
run_service_action "pre_$action" || die 13 "pre_$action failed!"
|
|
|
|
# Run the main action
|
|
run_service_action "$action"; res = "$?"
|
|
|
|
case "$action" in
|
|
stop)
|
|
result "$res" \
|
|
0 "Stopped $service_name" \
|
|
3 "$service_name is not running" \
|
|
5 "Operation timed out" \
|
|
7 "Can't “stop” a oneshot service. Use reset-exit if you want to “start” the service again"
|
|
;;
|
|
|
|
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" \
|
|
15 "Refusing to start $service_name: the service cgroup is not empty and \$service_cgroup_exclusive is set" \
|
|
17 "$service_name failed" \
|
|
19 "$service_name is already enabled"
|
|
;;
|
|
|
|
reload)
|
|
result "$res" \
|
|
0 "Reloading $service_name"
|
|
;;
|
|
|
|
status)
|
|
if service_oneshot; then
|
|
result "$res" \
|
|
0 "$service_name was successful" \
|
|
1 "$service_name is not enabled" \
|
|
9 "$service_name has failed ($service_exit_last)"
|
|
else
|
|
result "$res" \
|
|
0 "$service_name is running" \
|
|
1 "$service_name is not running" \
|
|
7 "$service_name was stopped" \
|
|
9 "$service_name has failed with code: $service_exit_last" \
|
|
11 "$service_name has exited with code: $service_exit_last"
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
(( res )) && exit "$res"
|
|
|
|
# Run post_$action function
|
|
run_service_action "post_$action" || die 15 "post_$action has failed!"
|