#!/usr/bin/env bash
shopt -s nullglob

# Utility functions
is_function() [[ "$(type -t "$1" 2>/dev/null)" == 'function' ]]
readonly -f is_function

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 3>"$service_logfile_out"
	else
		exec 3>"$service_logfile_err"
	fi

	exec "$@" >"$service_logfile_out" 2>&3
}; 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=0
		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

	info.item Name "$service_name"

	status; _status_code = "$?"
	if service_oneshot; then
		infoinfo.item Oneshot 'yes'

		case $_status_code in
			(0) _status = 'success';;
			(1) _status = 'not enabled';;
			(9) _status = "failed ($service_exit_last)";;
			(*) _status = 'unknown';;
		esac
	else
		info.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

	info.item Status "$_status"

	info.item Exec "${service_command[*]}"
	info.item Config "$service_config"
	info.item Workdir "$service_workdir"

	info.item 'Output log' "$service_logfile_out"
	service_logfile_out == "$service_logfile_err" || \
		info.item 'Error log'  "$service_logfile_err"

	if service_running; then
		info.item PID     "$service_pid"
		info.item PIDFile "$service_pidfile"

		info.item 'Running since' "$(stat -c '%y' "$service_pidfile")"

		cgroups && {
			info.item Cgroup          "$cgroup_home/$service_cgroup_name"
			info.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_config_current \
	service_tmpfiles \
	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
var flag_no_netns = 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_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

# 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';;

		(--no-netns)
			flag_no_netns = 1;;

		(--) 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
if (( UID )); then
	service_path += "$rundir/current" "$XDG_CONFIG_HOME/ssm/services" '/etc/ssm/services' "$usrdir/services"
else
	service_path += "$rundir/current" '/etc/ssm/services' "$usrdir/services"
fi

# 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_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"

# Rexec ourselves in the requested netns
flag_no_netns || {
	[[ $service_netns ]] && exec ip netns exec "$service_netns" "$0" --no-netns "$service_name" "$@"
}

# “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!"
