-
Notifications
You must be signed in to change notification settings - Fork 0
/
launcher.sh
executable file
·411 lines (348 loc) · 13.1 KB
/
launcher.sh
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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
#!/usr/bin/env bash
# Based on a template by BASH3 Boilerplate v2.0.0
# Copyright (c) 2013 Kevin van Zonneveld and contributors
# http://bash3boilerplate.sh/#authors
# Based on fork by WarpEngineer
# https://github.com/WarpEngineer/bash3boilerplate
#####################################
#
# A script to launch and manage system crons.
# usage: LOG_LEVEL={int} ./launcher.sh {options}
#
#####################################
# TODO: maybe as an option send mail if failed/succeeded/whatever
# TODO: need to redirect to log file??
###
# # Close STDOUT file descriptor
# exec 1<&-
# # Close STDERR FD
# exec 2<&-
#
# # Open STDOUT as $LOG_FILE file for read and write.
# exec 1<>$LOG_FILE
#
# # Redirect STDERR to STDOUT
# exec 2>&1
#
# echo "This line will appear in $LOG_FILE, not 'on screen'"
###
# Do not allow use of undefined vars. Use ${VAR:-} to use an undefined VAR
set -o nounset
# exit if anything returns an error
#set -o errexit
# Exit on error inside any functions or subshells.
set -o errtrace
# Catch the error in case mysqldump fails (but gzip succeeds) in `mysqldump |gzip`
set -o pipefail
# Turn on traces, useful while debugging but commented out by default
# set -o xtrace
# Set script version
__version="2024.04"
# Set magic variables for current file, directory, os, etc.
__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
__file="${__dir}/$(basename "${BASH_SOURCE[0]}")"
__base="$(basename ${__file} .sh)"
# Define the environment variables (and their defaults) that this script depends on
LOG_LEVEL="${LOG_LEVEL:-0}" # 7 = debug -> 0 = emergency
NO_COLOR="${NO_COLOR:-}" # true = disable color. otherwise autodetected
# grab defaults from environment if available, otherwise set them
# TODO: set defaults here as desired
[ -z "${DEFAULT_CONFIG_FILE_LOCATION:-}" ] && DEFAULT_CONFIG_FILE_LOCATION="."
[ -z "${DEFAULT_BIN_DIRECTORY:-}" ] && DEFAULT_BIN_DIRECTORY="."
### Functions
##############################################################################
function _fmt () {
local color_output="\x1b[36m"
local color_debug="\x1b[35m"
local color_info="\x1b[32m"
local color_notice="\x1b[34m"
local color_warning="\x1b[33m"
local color_error="\x1b[31m"
local color_critical="\x1b[1;31m"
local color_alert="\x1b[1;33;41m"
local color_emergency="\x1b[1;4;5;33;41m"
local colorvar=color_$1
local color="${!colorvar:-$color_error}"
local color_reset="\x1b[0m"
if [[ "${NO_COLOR:-}" = "true" ]] || ( [[ "${TERM:-}" != "xterm"* ]] && [[ "${TERM:-}" != "screen"* ]] ) || [[ ! -t 2 ]]; then
# Don't use colors on pipes or non-recognized terminals
color=""; color_reset=""
fi
echo -e "$(date -u +"%Y-%m-%d %H:%M:%S UTC") ${color}$(printf "[%9s]" ${1})${color_reset}";
}
function emergency () { echo "$(_fmt emergency) ${@}" 1>&2 || true; exit 1; }
function alert () { [ "${LOG_LEVEL}" -ge 1 ] && echo "$(_fmt alert) ${@}" 1>&2 || true; }
function critical () { [ "${LOG_LEVEL}" -ge 2 ] && echo "$(_fmt critical) ${@}" 1>&2 || true; }
function error () { [ "${LOG_LEVEL}" -ge 3 ] && echo "$(_fmt error) ${@}" 1>&2 || true; }
function warning () { [ "${LOG_LEVEL}" -ge 4 ] && echo "$(_fmt warning) ${@}" 1>&2 || true; }
function notice () { [ "${LOG_LEVEL}" -ge 5 ] && echo "$(_fmt notice) ${@}" 1>&2 || true; }
function info () { [ "${LOG_LEVEL}" -ge 6 ] && echo "$(_fmt info) ${@}" 1>&2 || true; }
function debug () { [ "${LOG_LEVEL}" -ge 7 ] && echo "$(_fmt debug) ${@}" 1>&2 || true; }
function output () { echo "$(_fmt output) ${@}" || true; }
function help () {
echo "" 1>&2
echo " ${@}" 1>&2
echo "" 1>&2
echo " ${__usage:-No usage available}" 1>&2
echo "" 1>&2
echo " ${__helptext:-}" 1>&2
echo "" 1>&2
exit 1
}
function cleanup_before_exit () {
info "Cleaning up. Done"
}
trap cleanup_before_exit EXIT
### Parse commandline options
##############################################################################
# Commandline options. This defines the usage page, and is used to parse cli
# opts & defaults from. The parsing is unforgiving so be precise in your syntax
# - A short option must be preset for every long option; but every short option
# need not have a long option
# - `--` is respected as the separator between options and arguments
# - We do not bash-expand defaults, so setting '~/app' as a default will not resolve to ${HOME}.
# you can use bash variables to work around this (so use ${HOME} instead)
read -r -d '' __usage <<-'EOF' || true # exits non-zero when EOF encountered
-c --configfile [arg] Configuration file to read.
-v Enable verbose mode, print script as it is executed
-d --debug Enables debug mode
-h --help This page
-n --no-color Disable color output
-V --version Show version and exit
EOF
read -r -d '' __helptext <<-'EOF' || true # exits non-zero when EOF encountered
A script that is launched by cron that manages other tasks. It is given a configuration file that
describes the task to manage. It will start the task with an option to only start one instance and keep
track of the PID and running state. This is to keep from having management code in each separate program.
The default location will be searched for config files if only a name is given. If the config file passed
begins with a slash (/) or dot (.) character, then it is assumed to be an absolute path and no search is made.
EOF
# Translate usage string -> getopts arguments, and set $arg_<flag> defaults
while read line; do
# fetch single character version of option string
opt="$(echo "${line}" |awk '{print $1}' |sed -e 's#^-##')"
# fetch long version if present
long_opt="$(echo "${line}" |awk '/\-\-/ {print $2}' |sed -e 's#^--##')"
long_opt_mangled="$(sed 's#-#_#g' <<< $long_opt)"
# map long name back to short name
varname="short_opt_${long_opt_mangled}"
eval "${varname}=\"${opt}\""
# check if option takes an argument
varname="has_arg_${opt}"
if ! echo "${line}" |egrep '\[.*\]' >/dev/null 2>&1; then
init="0" # it's a flag. init with 0
eval "${varname}=0"
else
opt="${opt}:" # add : if opt has arg
init="" # it has an arg. init with ""
eval "${varname}=1"
fi
opts="${opts:-}${opt}"
varname="arg_${opt:0:1}"
if ! echo "${line}" |egrep '\. Default=' >/dev/null 2>&1; then
eval "${varname}=\"${init}\""
else
match="$(echo "${line}" |sed 's#^.*Default=\(\)#\1#g')"
eval "${varname}=\"${match}\""
fi
done <<< "${__usage}"
# Allow long options like --this
opts="${opts}-:"
# Reset in case getopts has been used previously in the shell.
OPTIND=1
# start parsing command line
set +o nounset # unexpected arguments will cause unbound variables
# to be dereferenced
# Overwrite $arg_<flag> defaults with the actual CLI options
while getopts "${opts}" opt; do
[ "${opt}" = "?" ] && help "Invalid use of script: ${@} "
if [ "${opt}" = "-" ]; then
# OPTARG is long-option-name or long-option=value
if [[ "${OPTARG}" =~ .*=.* ]]; then
# --key=value format
long=${OPTARG/=*/}
long_mangled="$(sed 's#-#_#g' <<< $long)"
# Set opt to the short option corresponding to the long option
eval "opt=\"\${short_opt_${long_mangled}}\""
OPTARG=${OPTARG#*=}
else
# --key value format
# Map long name to short version of option
long_mangled="$(sed 's#-#_#g' <<< $OPTARG)"
eval "opt=\"\${short_opt_${long_mangled}}\""
# Only assign OPTARG if option takes an argument
eval "OPTARG=\"\${@:OPTIND:\${has_arg_${opt}}}\""
# shift over the argument if argument is expected
((OPTIND+=has_arg_${opt}))
fi
# we have set opt/OPTARG to the short value and the argument as OPTARG if it exists
fi
varname="arg_${opt:0:1}"
default="${!varname}"
value="${OPTARG}"
if [ -z "${OPTARG}" ] && [ "${default}" = "0" ]; then
value="1"
fi
eval "${varname}=\"${value}\""
debug "cli arg ${varname} = ($default) -> ${!varname}"
done
set -o nounset # no more unbound variable references expected
shift $((OPTIND-1))
[ "${1:-}" = "--" ] && shift
### Command-line argument switches (like -d for debugmode, -h for showing helppage)
##############################################################################
# debug mode
if [ "${arg_d}" = "1" ]; then
set -o xtrace
LOG_LEVEL="7"
fi
# verbose mode
if [ "${arg_v}" = "1" ]; then
set -o verbose
fi
# no color mode
if [ "${arg_n}" = "1" ]; then
NO_COLOR="true"
fi
# version mode
if [ "${arg_V}" = "1" ]; then
# Version print exists with code 1
echo "Version: ${__version}" 2>&1
exit 0
fi
# help mode
if [ "${arg_h}" = "1" ]; then
# Help exists with code 1
help "Help using ${0}"
fi
### Validation. Error out if the things required for your script are not present
################################################################################
[ -z "${arg_c:-}" ] && emergency "can not continue without a config file. "
[ -z "${LOG_LEVEL:-}" ] && emergency "can not continue without LOG_LEVEL. "
# read configuration file and verify
# check if arg_c begins with a / and treat as absolute path if so. Otherwise, look under default location.
if [[ ! "${arg_c}" == /* ]]
then
if [[ ! "${arg_c}" == .* ]]
then
# relative path
arg_c="${DEFAULT_CONFIG_FILE_LOCATION}/${arg_c}"
fi
fi
# set task defaults
Parameters=""
Active="true"
Allow_Multiple="false"
Trigger_Script=""
Comment=""
Log_File=""
[ ! -r ${arg_c} ] && emergency "can not read configuration file ${arg_c}"
. ${arg_c}
[ -z "${Task_Name:-}" ] && emergency "can not continue without Task_Name. "
[ -z "${Application_Name:-}" ] && emergency "can not continue without Application_Name. "
# make sure working direcotry is something
[ -z "${Working_Directory:-}" ] && Working_Directory="${PWD}"
# check if applcation is an absolute path. if not, use default location
if [[ ! "${Application_Name}" == /* ]]
then
# relative path
Application_Name="${DEFAULT_BIN_DIRECTORY}/${Application_Name}"
fi
[ ! -x "${Application_Name:-}" ] && emergency "can not continue because ${Application_Name} is not found or not executable. "
[ ! -d "${Working_Directory:-}" ] && emergency "can not continue because ${Working_Directory} does not exist. "
if [ -n "${Trigger_Script}" ]
then
[ ! -x "${Trigger_Script:-}" ] && emergency "can not continue because ${Trigger_Script} is not found or not executable. "
fi
### Runtime
##############################################################################
info "__file: ${__file}"
info "__dir: ${__dir}"
info "__base: ${__base}"
info "OSTYPE: ${OSTYPE}"
info "arg_c: ${arg_c}"
info "arg_d: ${arg_d}"
info "arg_v: ${arg_v}"
info "arg_h: ${arg_h}"
info "arg_n: ${arg_n}"
info "Task_Name="${Task_Name}
info "Application_Name="${Application_Name}
info "Parameters="${Parameters}
info "Working_Directory="${Working_Directory}
info "Trigger_Script="${Trigger_Script}
info "Active="${Active}
info "Allow_Multiple="${Allow_Multiple}
info "Comment="${Comment}
info "Log_File="${Log_File}
# if the task is not active, do nothing and just exit cleanly
if [ ! "${Active}" = "true" ]
then
info "Task is not active. Exiting..."
exit 0
fi
# if a trigger script is included, run it to check if the task is triggered
if [ -n "${Trigger_Script}" ]
then
if ! "${Trigger_Script}"
then
info "Trigger script returned unsuccessful so run condition not met. Exiting..."
exit 0
fi
fi
function run_task () {
CWD=$PWD
cd "${Working_Directory}"
debug "Running ${Application_Name} ${Parameters} in directory ${Working_Directory}"
if [ -n "${Log_File:-}" ]
then
"${Application_Name}" ${Parameters} >${Log_File} 2>&1 &
else
"${Application_Name}" ${Parameters} &
fi
PID=$!
debug "Started task with pid ${PID}"
echo "${PID}" > "${PID_FILE}"
debug "Wating for task to finish..."
wait "${PID}"
RESULT=$?
info "Task completed with exit code ${RESULT}"
rm "${PID_FILE}"
cd "${CWD}"
}
# create an ID for this instance
INSTANCE_ID="$( echo "${Task_Name}_${Application_Name}_${Parameters}" | (sha512sum || sha256sum || sha1sum || shasum) | cut -f1 -d' ' )"
debug "INSTANCE_ID"=${INSTANCE_ID}
# run directory for this instance
[ -z "${DEFAULT_RUN_DIRECTORY:-}" ] && DEFAULT_RUN_DIRECTORY=${Working_Directory}
RUN_DIRECTORY="${DEFAULT_RUN_DIRECTORY}/${INSTANCE_ID}"
mkdir -p "${RUN_DIRECTORY}"
[ ! -d "${RUN_DIRECTORY:-}" ] && emergency "can not continue because ${RUN_DIRECTORY} could not be created. "
debug "RUN_DIRECTORY"=${RUN_DIRECTORY}
# check run directory for a PID file. if it exists, check to see if it's still running. if still running and Allow_Multiple is false
# then this instance must exit. if Allow_Multiple is true, then keep going.
if [ "${Allow_Multiple}" = "false" ]
then
PID_FILE="${RUN_DIRECTORY}/pid"
debug "PID_FILE="${PID_FILE}
if [ -f "${PID_FILE}" ]
then
PID="$( cat ${PID_FILE} )"
RUNNING_PIDS="$( pgrep -f $( basename ${Application_Name} ) || echo -1 )"
#if [ "${PID}" -eq "${RUNNING_PID}" ]
if [[ ${RUNNING_PIDS} =~ (^|[[:space:]])${PID}($|[[:space:]]) ]]
then
# still running so just exit
info "Task is already running and Allow_Multiple is false. Exiting..."
exit 0
else
# maybe it crashed, so just take over
run_task
fi
else
run_task
fi
else
PID_FILE="${RUN_DIRECTORY}/pid$$"
debug "PID_FILE="${PID_FILE}
run_task
fi