xref: /openbsd/etc/rc.d/rc.subr (revision 3bef86f7)
1#	$OpenBSD: rc.subr,v 1.162 2024/01/17 08:26:06 ajacoutot Exp $
2#
3# Copyright (c) 2010, 2011, 2014-2022 Antoine Jacoutot <ajacoutot@openbsd.org>
4# Copyright (c) 2010, 2011 Ingo Schwarze <schwarze@openbsd.org>
5# Copyright (c) 2010, 2011, 2014 Robert Nagy <robert@openbsd.org>
6#
7# Permission to use, copy, modify, and distribute this software for any
8# purpose with or without fee is hereby granted, provided that the above
9# copyright notice and this permission notice appear in all copies.
10#
11# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
12# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
13# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
14# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
15# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
16# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
17# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
18
19_rc_actions="start stop restart reload check configtest"
20readonly _rc_actions
21
22_rc_check_name() {
23	[[ $1 == +([_[:alpha:]])+(|[_[:alnum:]]) ]]
24}
25
26_rc_do() {
27	if [ -n "${_RC_DEBUG}" ]; then
28		echo "doing $@" && "$@"
29	else
30		"$@" >/dev/null 2>&1
31	fi
32}
33
34_rc_err() {
35	[ -n "${1}" ] && echo "${1}" 1>&2
36	[ -n "${2}" ] && exit "${2}" || exit 1
37}
38
39_rc_parse_conf() {
40	typeset -l _key
41	local _l _rcfile _val
42	set -A _allowed_keys -- \
43		accounting amd_master check_quotas ipsec library_aslr \
44		multicast nfs_server pexp pf pkg_scripts shlib_dirs spamd_black
45
46	[ $# -gt 0 ] || set -- /etc/rc.conf /etc/rc.conf.local
47	for _rcfile; do
48		[[ -f $_rcfile ]] || continue
49		while IFS=' 	' read -r _l; do
50			[[ $_l == [!#=]*=* ]] || continue
51			_key=${_l%%*([[:blank:]])=*}
52			[[ $_key == *_@(execdir|flags|logger|rtable|timeout|user) ]] ||
53				[[ " ${_allowed_keys[*]} " == *" $_key "* ]] ||
54				continue
55			[[ $_key == "" ]] && continue
56			_val=${_l##*([!=])=*([[:blank:]])}
57			_val=${_val%%#*}
58			_val=${_val%%*([[:blank:]])}
59			# remove leading and trailing quotes (backwards compat)
60			[[ $_val == @(\"*\"|\'*\') ]] &&
61				_val=${_val#?} _val=${_val%?}
62			eval "${_key}=\${_val}"
63		done < $_rcfile
64	done
65
66	# special care needed for spamlogd to avoid starting it up and failing
67	# all the time
68	if [ X"${spamd_flags}" = X"NO" -o X"${spamd_black}" != X"NO" ]; then
69		spamlogd_flags=NO
70	fi
71
72	# special care needed for pflogd to avoid starting it up and failing
73	# if pf is not enabled
74	if [ X"${pf}" = X"NO" ]; then
75		pflogd_flags=NO
76	fi
77
78	# special care needed if nfs_server=YES to startup nfsd and mountd with
79	# sane default flags
80	if [ X"${nfs_server}" = X"YES" ]; then
81		[ X"${nfsd_flags}" = X"NO" ] && nfsd_flags="-tun 4"
82		[ X"${mountd_flags}" = X"NO" ] && mountd_flags=
83	fi
84}
85
86# return if we only want internal functions
87[ -n "${FUNCS_ONLY}" ] && return
88
89_rc_not_supported() {
90	local _a _enotsup _what=${1}
91	for _a in ${_rc_actions}; do
92		[ "${_what}" == "configtest" ] &&
93			! typeset -f rc_configtest >/dev/null && _enotsup=NO &&
94			break
95		[ "${_what}" == "restart" ] && _what="stop"
96		if [ "${_what}" == "${_a}" ]; then
97			eval _enotsup=\${rc_${_what}}
98			break
99		fi
100	done
101	[ X"${_enotsup}" == X"NO" ]
102}
103
104_rc_usage() {
105	local _a _allsup
106	for _a in ${_rc_actions}; do
107		_rc_not_supported ${_a} || _allsup="${_allsup:+$_allsup|}${_a}"
108	done
109	_rc_err "usage: $0 [-df] ${_allsup}"
110}
111
112_rc_write_runfile() {
113	[ -d ${_RC_RUNDIR} ] || mkdir -p ${_RC_RUNDIR} &&
114		cat >${_RC_RUNFILE} <<EOF
115daemon_class=${daemon_class}
116daemon_execdir=${daemon_execdir}
117daemon_flags=${daemon_flags}
118daemon_logger=${daemon_logger}
119daemon_rtable=${daemon_rtable}
120daemon_timeout=${daemon_timeout}
121daemon_user=${daemon_user}
122pexp=${pexp}
123rc_reload=${rc_reload}
124rc_reload_signal=${rc_reload_signal}
125rc_stop_signal=${rc_stop_signal}
126rc_usercheck=${rc_usercheck}
127EOF
128}
129
130_rc_rm_runfile() {
131	rm -f ${_RC_RUNFILE}
132}
133
134_rc_exit() {
135	local _pfix
136	[ -z "${INRC}" -o X"$1" != X"ok" ] && _pfix="($1)"
137	echo ${INRC:+'-n'} "${_pfix}"
138	[[ $1 == @(ok|killed) ]] && exit 0 || exit 1
139}
140
141_rc_alarm()
142{
143	trap - ALRM
144	kill -ALRM ${_TIMERSUB} 2>/dev/null # timer may not be running anymore
145	kill $! 2>/dev/null # kill last job if it's running
146}
147
148_rc_sendsig() {
149	pkill -${1:-TERM} -T "${daemon_rtable}" -xf "${pexp}"
150}
151
152_rc_wait_for_start() {
153	trap "_rc_alarm" ALRM
154	while ((SECONDS < daemon_timeout)); do
155		if _rc_do rc_check; then
156			[ X"${rc_bg}" = X"YES" ] || [ -z "$$" ] && break
157		fi
158		sleep .5
159	done & wait
160	pkill -ALRM -P $$
161	return
162}
163
164rc_exec() {
165	local _rcexec="su -fl -c ${daemon_class} -s /bin/sh ${daemon_user} -c"
166	[ "${daemon_rtable}" -eq "$(id -R)" ] ||
167		_rcexec="route -T ${daemon_rtable} exec ${_rcexec}"
168
169	local _set_monitor=":"
170	# Run non-daemons services in a different process group to avoid SIGHUP
171	# at boot.
172	if [ X"${rc_bg}" = X"YES" ]; then
173		_set_monitor="set -o monitor"
174	fi
175
176	${_rcexec} "${_set_monitor}; \
177		${daemon_logger:+set -o pipefail; } \
178		${daemon_execdir:+cd ${daemon_execdir} && } \
179		$@ \
180		${daemon_logger:+ 2>&1 |
181			logger -isp ${daemon_logger} -t ${_name}}"
182}
183
184rc_start() {
185	rc_exec "${daemon} ${daemon_flags}"
186}
187
188rc_check() {
189	pgrep -T "${daemon_rtable}" -q -xf "${pexp}"
190}
191
192rc_reload() {
193	_rc_sendsig ${rc_reload_signal}
194}
195
196rc_stop() {
197	_rc_sendsig ${rc_stop_signal}
198}
199
200rc_cmd() {
201	local _exit _n _ret _timer
202	# optim: don't sleep(1) in the first loop
203	_1stloop=true
204
205	[ -n "${1}" ] && echo "${_rc_actions}" | grep -qw -- ${1} || _rc_usage
206
207	[ "$(id -u)" -eq 0 ] ||
208		[ X"${rc_usercheck}" != X"NO" -a X"$1" = "Xcheck" ] ||
209		_rc_err "$0: need root privileges"
210
211	if _rc_not_supported $1; then
212		[ -n "${INRC}" ] && exit 1
213		_rc_err "$0: $1 is not supported"
214	fi
215
216	[ -n "${_RC_DEBUG}" ] || _n="-n"
217
218	[[ ${1} == start ]] || _rc_do _rc_parse_conf ${_RC_RUNFILE}
219
220	case "$1" in
221	check)
222		echo $_n "${INRC:+ }${_name}"
223		_rc_do rc_check && _rc_exit ok || _rc_exit failed
224		;;
225	configtest)
226		echo $_n "${INRC:+ }${_name}"
227		_rc_do rc_configtest && _rc_exit ok || _rc_exit failed
228		;;
229	start)
230		if [ X"${daemon_flags}" = X"NO" ]; then
231			_rc_err "$0: need -f to force $1 since ${_name}_flags=NO"
232		fi
233		[ -z "${INRC}" ] && _rc_do rc_check && exit 0
234		echo $_n "${INRC:+ }${_name}"
235		while true; do # no real loop, only needed to break
236			# running during start is mostly useful for daemons
237			# whose child will not return a config parsing error to
238			# the parent during startup; e.g. bgpd, httpd...
239			if typeset -f rc_configtest >/dev/null; then
240				_rc_do rc_configtest || break
241			fi
242			if typeset -f rc_pre >/dev/null; then
243				_rc_do rc_pre || break
244			fi
245			# prevent hanging the boot sequence
246			_rc_do _rc_wait_for_start & _TIMERSUB=$!
247			trap "_rc_alarm" ALRM
248			_rc_do rc_start; _ret=$?
249			kill -ALRM ${_TIMERSUB}
250			wait ${_TIMERSUB} 2>/dev/null # don't print Alarm clock
251			# XXX for unknown reason, rc_check can fail (e.g. redis)
252			# while it just succeeded in _rc_wait_for_start;
253			# check to cope with failing daemons returning 0
254			#[[ "${_ret}" == @(0|142) ]] && _rc_do rc_check || break
255			[[ "${_ret}" == @(0|142) ]] || break
256			[[ "${_ret}" == 142 ]] && [ X"${rc_bg}" != X"YES" ] &&
257				_exit="timeout"
258			_rc_do _rc_write_runfile
259			_rc_exit ${_exit:=ok}
260		done
261		# handle failure
262		_rc_do _rc_rm_runfile
263		typeset -f rc_post >/dev/null && _rc_do rc_post
264		_rc_exit failed
265		;;
266	stop)
267		_rc_do rc_check || exit 0
268		echo $_n "${INRC:+ }${_name}"
269		_rc_do rc_stop & _timer=$!
270		while ((SECONDS < daemon_timeout)); do
271			# last chance: send a SIGTERM first in case the process
272			# used another signal to stop (e.g. SIGQUIT with nginx)
273			# or a non-default rc_stop() function; do it 2s before
274			# timeout to re-enter the loop one last time which will
275			# give 1s for SIGTERM to terminate the process
276			((SECONDS == daemon_timeout-2)) &&
277				_rc_do _rc_sendsig TERM && sleep .5
278			pkill -0 -P "$$" 2>/dev/null || _rc_do rc_check ||
279				break
280			${_1stloop} && _1stloop=false || sleep .5
281		done
282		kill -ALRM ${_timer} 2>/dev/null
283		wait ${_timer} # don't print Alarm clock
284		[[ $? == 0 ]] || _exit=failed
285		# KILL the process
286		_rc_do rc_check && _rc_do _rc_sendsig KILL && _exit="killed"
287		_rc_do _rc_rm_runfile
288		if typeset -f rc_post >/dev/null; then
289			_rc_do rc_post || _exit=failed
290		fi
291		_rc_exit ${_exit:=ok}
292		;;
293	reload)
294		echo $_n "${INRC:+ }${_name}"
295		_rc_do rc_check || _rc_exit failed
296		if typeset -f rc_configtest >/dev/null; then
297			_rc_do rc_configtest || _rc_exit failed
298		fi
299		_rc_do rc_reload & _timer=$!
300		while ((SECONDS < daemon_timeout)); do
301			pkill -0 -P "$$" 2>/dev/null || break
302			${_1stloop} && _1stloop=false || sleep .5
303		done
304		kill -ALRM ${_timer} 2>/dev/null
305		wait ${_timer} # don't print Alarm clock
306		_ret=$?
307		[[ ${_ret} == 142 ]] && _exit=timeout || [[ ${_ret} == 0 ]] ||
308			_exit=failed
309		_rc_exit ${_exit:=ok}
310		;;
311	restart)
312		if typeset -f rc_configtest >/dev/null; then
313			_rc_do rc_configtest || _rc_exit failed
314		fi
315		$0 ${_RC_DEBUG} ${_RC_FORCE} stop &&
316			$0 ${_RC_DEBUG} ${_RC_FORCE} start
317		;;
318	*)
319		_rc_usage
320		;;
321	esac
322}
323
324_name=${0##*/}
325_rc_check_name "${_name}" || _rc_err "invalid rc.d script name: ${_name}"
326
327[ -n "${KSH_VERSION}" ] || _rc_err "$0: wrong shell, use /bin/ksh"
328[ -n "${daemon}" ] || _rc_err "$0: daemon is not set"
329
330unset _RC_DEBUG _RC_FORCE
331while getopts "df" c; do
332	case "$c" in
333		d) _RC_DEBUG=-d;;
334		f) _RC_FORCE=-f;;
335		*) _rc_usage;;
336	esac
337done
338shift $((OPTIND-1))
339
340_RC_RUNDIR=/var/run/rc.d
341_RC_RUNFILE=${_RC_RUNDIR}/${_name}
342
343# parse /etc/rc.conf{.local} for the daemon variables
344_rc_do _rc_parse_conf
345
346rc_reload_signal=${rc_reload_signal:=HUP}
347rc_stop_signal=${rc_stop_signal:=TERM}
348
349eval _rcexecdir=\${${_name}_execdir}
350eval _rcflags=\${${_name}_flags}
351eval _rclogger=\${${_name}_logger}
352eval _rcrtable=\${${_name}_rtable}
353eval _rctimeout=\${${_name}_timeout}
354eval _rcuser=\${${_name}_user}
355
356# set default values; duplicated in rcctl(8)
357getcap -f /etc/login.conf.d/${_name}:/etc/login.conf ${_name} 1>/dev/null 2>&1 && \
358	daemon_class=${_name} || daemon_class=daemon
359[ -z "${daemon_rtable}" ] && daemon_rtable=0
360[ -z "${daemon_timeout}" ] && daemon_timeout=30
361[ -z "${daemon_user}" ] && daemon_user=root
362
363# use flags from the rc.d script if daemon is not enabled
364[ -n "${_RC_FORCE}" -o "$1" != "start" ] && [ X"${_rcflags}" = X"NO" ] &&
365	unset _rcflags
366
367[ -n "${_rcexecdir}" ] && daemon_execdir=${_rcexecdir}
368[ -n "${_rcflags}" ] && daemon_flags=${_rcflags}
369[ -n "${_rclogger}" ] && daemon_logger=${_rclogger}
370[ -n "${_rcrtable}" ] && daemon_rtable=${_rcrtable}
371[ -n "${_rctimeout}" ] && daemon_timeout=${_rctimeout}
372[ -n "${_rcuser}" ] && daemon_user=${_rcuser}
373
374if [ -n "${_RC_DEBUG}" ]; then
375	echo -n "${_name}_flags "
376	[ -n "${_rcflags}" ] || echo -n "empty, using default "
377	echo ">${daemon_flags}<"
378fi
379
380readonly daemon_class
381unset _rcexecdir _rcflags _rclogger _rcrtable _rctimeout _rcuser
382# the shell will strip the quotes from daemon_flags when starting a daemon;
383# make sure pexp matches the process (i.e. doesn't include the quotes)
384pexp="$(eval echo ${daemon}${daemon_flags:+ ${daemon_flags}})"
385