summaryrefslogtreecommitdiff
blob: 42c88100df1c0ac10195a5302dd53c3294024054 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
#!/bin/sh
# Copyright 1999-2015 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
#
# this script looks into /etc/cron.[hourly|daily|weekly|monthly]
# for scripts to be executed. The info about last run is stored in
# /var/spool/cron/lastrun

LOCKDIR="/var/lock"
CRONSPOOLDIR="/var/spool/cron"
LASTRUNDIR="${CRONSPOOLDIR}/lastrun"
# This is the legacy lockfile that we need to clean up.
GLOBAL_LOCKFILE="${LASTRUNDIR}/lock"

# Usage: log <level> <args to logger>
# Log a message via syslog.
log() {
	local level="$1"
	shift
	logger -i -p "cron.${level}" -t run-crons "$@"
}

# Usage: grab_lock <class>
# Grab the lock for <class> to make sure we are the only instance.
grab_lock() {
	local i cronpid cmdline1 cmdline2
	local lockfile

	# Free whatever previous lock (if any) we held.
	free_lock

	# For the legacy global lock, don't try to create a full path.
	case $1 in
	/*) lockfile=$1 ;;
	*)  lockfile="${LOCKDIR}/cron.$1" ;;
	esac

	# Try twice to lock, otherwise give up.
	i=0
	while [ $(( i += 1 )) -le 2 ] ; do
		# Normally we should be able to grab the lock and get out of here fast.
		if ln -sn $$ "${lockfile}" 2>/dev/null ; then
			break
		fi

		# Locking failed, so check for a running process.
		# Handle both old- and new-style locking.
		# Delete the cat logic when GLOBAL_LOCKFILE is purged.
		# Note: Does not handle PID namespaces ...
		if ! cronpid=$(readlink "${lockfile}" 2>/dev/null) ; then
			if ! cronpid=$(cat "${lockfile}" 2>/dev/null) ; then
				# The lockfile disappeared?  Try the whole thing again ...
				continue
			fi
		fi

		# This is better than kill -0 because we can verify that it's really
		# another run-crons process.
		# We have to send stderr to /dev/null for two reasons:
		# - If the process disappears, the cmdline file might not exist.
		# - The cmdline file contains NUL bytes, but bash-4.4+ warns when
		#   you try to assign NUL bytes to variables.
		# It'd be nice to not do it for a lot of code, but there's not easy
		# alternative in shell code.  We could `cat | tr`, but that'd waste
		# a bit more than just a simple cat.
		if (
			cmdline1=$(cat "/proc/${cronpid}/cmdline") || :
			cmdline2=$(cat "/proc/$$/cmdline")
			[ "${cmdline1}" = "${cmdline2}" ]
		) 2>/dev/null ; then
			# Whoa, another run-crons is really running.
			return 1
		fi

		# The lockfile is pointing to a dead process so break it.
		# TODO: This is still racy if we're running more than one run-crons.
		rm -f "${lockfile}"
	done

	# Check to make sure locking was successful.
	if [ ! -L "${lockfile}" ] ; then
		echo "Can't create or read existing ${lockfile}, giving up"
		exit 1
	fi

	# Set the lock file for free_lock to clean up.
	_LOCKFILE="${lockfile}"

	return 0
}
# Prevent random env vars from messing with us.
_LOCKFILE=
# Set a trap to release the lockfile when we're finished.
trap 'free_lock' EXIT HUP INT QUIT TERM

# Usage: free_lock
# Release the lock that we last grabbed.  This does not nest!
free_lock() {
	if [ -n "${_LOCKFILE}" ] ; then
		rm -f "${_LOCKFILE}"
		# Only break the lock once.
		_LOCKFILE=
	fi
}


EXIT_STATUS=0

# Grab the legacy global lock to smoothly handle upgrades.
# We should drop this after like Dec 2016.
if [ -L "${GLOBAL_LOCKFILE}" -o -f "${GLOBAL_LOCKFILE}" ] ; then
	if ! grab_lock "${GLOBAL_LOCKFILE}" ; then
		# An old process is still running -- abort.
		exit 0
	fi
	# Now release the lock since we no longer care about it.
	free_lock
fi

for BASE in hourly daily weekly monthly ; do
	CRONDIR=/etc/cron.${BASE}

	test -d $CRONDIR || continue

	# Grab the lock for this specific dir.
	if ! grab_lock "${BASE}" ; then
		# Someone else is processing this dir, so skip it.
		continue
	fi

	# Blow away stale states for this particular dir.
	lastrunfile="${LASTRUNDIR}/cron.${BASE}"
	if [ -e "${lastrunfile}" ] ; then
		case $BASE in
		hourly)
			#>= 1 hour, 5 min -=> +65 min
			TIME="-cmin +65" ;;
		daily)
			#>= 1 day, 5 min -=> +1445 min
			TIME="-cmin +1445"  ;;
		weekly)
			#>= 1 week, 5 min -=> +10085 min
			TIME="-cmin +10085"  ;;
		monthly)
			#>= 31 days, 5 min -=> +44645 min
			TIME="-cmin +44645" ;;
		esac

		find "${LASTRUNDIR}/" -name cron.$BASE $TIME -exec rm {} \; 2>/dev/null || :
	fi

	# if there is no state file, make one, then run the scripts.
	if [ ! -e "${lastrunfile}" ] ; then
		touch "${lastrunfile}"

		set +e
		for SCRIPT in $CRONDIR/* ; do
			if [ -x "${SCRIPT}" ] && [ ! -d "${SCRIPT}" ] ; then
				# Filter out files people do not expect to be executed.
				case ${SCRIPT} in
				.*|*~) continue ;;
				esac

				log info "($(whoami)) CMD (${SCRIPT})"
				$SCRIPT
				ret=$?
				if [ ${ret} -ne 0 ] ; then
					log err "CMD (${SCRIPT}) failed with exit status ${ret}"
					EXIT_STATUS=1
				fi
			fi
		done
	fi
done

# Clean out bogus state files with future times.
touch "${LASTRUNDIR}"
find "${LASTRUNDIR}/" -newer "${LASTRUNDIR}" -exec /bin/rm -f {} \; 2>/dev/null || :

exit ${EXIT_STATUS}