-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtasklib.sh
331 lines (284 loc) · 9.09 KB
/
tasklib.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
#!/usr/bin/env bash
# Usage:
# Sourcing this file implicitly initializes the library
#
# Tasks:
# This library is used to order execution of 'tasks', where each task
# is a simple bash function. **Tasks must be idempotent**
# Tasks can depend on the completion of other tasks.
# Given a bash function 'bar' that needs to run after function 'foo', they
# can be registered as:
# task foo
# task bar foo
# task <function_name> <dep_0> <dep_1> ... <dep_n>
#
# Task environment:
# Tasks share the same environment as the setup script that invokes them
# Furthermore, any output to stdout or stderr is redirected to the debug log, and
# stdin is redirected from /dev/null.
#
# Running Tasks:
# A defined task can be run by using the 'run_task' function. The 'run_task'
# function will only accept a single task at a time, but can be invoked multiple
# times. Any task that returns a non-zero exit status will be marked as failed
# and all subsequent tasks will be skipped.
#
# Bash Traps:
# This library takes over the EXIT signal trap to run 'tlib_cleanup'
# You are free to override this, but in that case, it is your responsibility to
# ensure 'tlib_cleanup' is run in any possible exit condition. Zombie
# processes will be left beind if this does not happen
#
# If you would like to run a custom cleanup function, your function can be inserted
# into the array TLIB_CLEANUP_HOOKS. These functions are run in reverse order from
# within the 'tlib_cleanup' function
#
# Debug Logging:
# After this library is sourced/initialized, the 'tlib_debug' method is available
# until 'tlib_cleanup' is run. It accepts a string and writes it directly into
# the debug log
#
# Aborting:
# The tasks are run in the same process as the one that invokes 'run_task'. Any
# task can abort and cancel the remaining tasks by returning a non-zero exit code.
# In an emergency, the 'tlib_error_exit' method is available as well. This method
# will print the provided message to both the user and the debug log along with a
# stack trace, and kill the process by calling exit 1
#
# Circular Dependencies:
# Circular dependencies are not detected by this library, and will not cause an error,
# and the execution order of the tasks are not guaranteed to be correct.
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
# This script has been executed instead of sourced
echo "Please source this file!" >&2
exit 1
fi
# Enable job control (required for background process handling in log writer)
set -m
TLIB_CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >>/dev/null && pwd)"
tlib_debug() {
echo "DEBUG: $*" >> "$TLIB_LOG_FILE"
}
tlib_findin() {
local arrayname tmp array item
arrayname=$1
tmp="$arrayname[@]"
array=( "${!tmp}" )
for item in "${array[@]}"; do
[[ "$item" == "$2" ]] && return 0
done
return 1
}
tlib_cleanup() {
local idx taskName taskType ret
# Save stdout/stderr and modify it to write to the log and the user
# Because this is asynchronous to the cleanup task execution output
# for the previous cleanup task will likely be logged after the next
# cleanup task has started
exec 11>&1
exec 12>&2
exec 1> >(tee -a >(awk '{print "DEBUG: stdout: "$0; fflush()}' >> "$TLIB_LOG_FILE"))
exec 2> >(tee -a >(awk '{print "DEBUG: stderr: "$0; fflush()}' >> "$TLIB_LOG_FILE") >&12)
for (( idx=${#TLIB_CLEANUP_HOOKS[@]}-1 ; idx >= 0 ; idx-- )) ; do
taskName="${TLIB_CLEANUP_HOOKS[idx]}"
taskType="$(type -t $taskName)"
tlib_debug "Running cleanup task: $taskName"
if [[ "$taskType" != "function" ]]; then
tlib_debug "$taskName is not a function... skipping"
else
$taskName < /dev/null
ret=$?
tlib_debug "$taskName completed with return code $ret"
fi
done
# Restore file descriptors
exec 1>&11
exec 2>&12
exec 11<&-
exec 12<&-
# Explicit user-facing messages here
echo "Debug log written to $TLIB_LOG_FILE"
}
tlib_initialize() {
TLIB_TEMP_DIR="$(mktemp -dq)"
TLIB_LOG_FILE="$TLIB_TEMP_DIR/tasklib.log"
TLIB_CLEANUP_HOOKS=()
# Set up the cleanup hooks to run on exit
trap tlib_cleanup EXIT
tlib_debug "===== New Execution ($(date)) ====="
# Initialize components and their cleanup hooks
tlib_initialize_log_writer
tlib_initialize_dot_file
tlib_debug "Using workdir $TLIB_CURRENT_DIR"
TLIB_TASK_ARRAY_PREFIX="tlib_task_dependency__"
tlib_registered_tasks=()
unset -f "tlib_initialize"
}
tlib_initialize_log_writer() {
local output_pipe
# Set up the log pipe
output_pipe="$TLIB_TEMP_DIR/pipe"
mkfifo "$output_pipe"
exec 10<> "$output_pipe"
rm "$output_pipe"
tlib_debug "Using fd 10 for log $TLIB_LOG_FILE"
# Read from FD 10 and write to debug log
(cat <&10 | while read -r output; do
tlib_debug "task output: $output"
done) &
TLIB_BG_PGID="$!"
tlib_debug "Log writer initialized with pgid $TLIB_BG_PGID"
tlib_cleanup_log_writer() {
tlib_debug "Killing log writer pgid $TLIB_BG_PGID"
kill -- -$TLIB_BG_PGID
tlib_debug "Closing fd 10"
exec 10<&-
}
TLIB_CLEANUP_HOOKS+=(tlib_cleanup_log_writer)
}
tlib_initialize_dot_file() {
TLIB_DOT_FILE="$TLIB_TEMP_DIR/tasks.dot"
echo "digraph tasks {" > "$TLIB_DOT_FILE"
tlib_finalize_dot_file() {
echo "}" >> "$TLIB_DOT_FILE"
echo "Dot file written to $TLIB_DOT_FILE"
}
TLIB_CLEANUP_HOOKS+=(tlib_finalize_dot_file)
}
tlib_error_exit() {
echo "ERROR: $*" | tee -a "$TLIB_LOG_FILE" >&2
local frame
frame=0
while true; do
local trace
trace="$(caller $frame)"
[[ -z "$trace" ]] && break
awk '{printf " from %s at %s:%d\n", $2, $3, $1}' <<< "$trace" | tee -a "$TLIB_LOG_FILE" >&2
((frame++))
done
exit 1
}
tlib_task_is_defined() {
tlib_findin "tlib_registered_tasks" "$1"
}
tlib_assert_task_is_defined() {
while [[ "$#" -gt 0 ]]; do
tlib_task_is_defined "$1" || tlib_error_exit "task($1) is not defined"
shift
done
}
tlib_get_dependency_array_name() {
echo "$TLIB_TASK_ARRAY_PREFIX$1"
}
tlib_register_dependency() {
local taskName arrayname tmp array
taskName="$1"
shift
tlib_assert_task_is_defined "$taskName"
arrayname="$(tlib_get_dependency_array_name "$taskName")"
tmp="$arrayname[@]"
array=( "${!tmp}" )
while [[ "$#" -gt 0 ]]; do
tlib_debug "registering depdency $taskName -> $1"
echo -e "\\t$taskName -> $1;" >> "$TLIB_DOT_FILE"
array+=("$1")
shift
done
eval "$arrayname=(\"\${array[@]}\")"
}
tlib_get_dependencies_recursive() {
local taskName task_list arrayname tmp array subtask list dep
taskName="$1"
task_list=()
arrayname="$(tlib_get_dependency_array_name "$taskName")"
tmp="$arrayname[@]"
array=( "${!tmp}" )
tlib_debug "getting dependencies for task $taskName"
for subtask in "${array[@]}"; do
list=( $(tlib_get_dependencies_recursive "$subtask") )
for dep in "${list[@]}"; do
if ! tlib_findin "task_list" "$dep"; then
task_list+=("$dep")
fi
done
if ! tlib_findin "task_list" "$subtask"; then
task_list+=("$subtask")
fi
done
echo "${task_list[@]}"
}
task() {
local taskType taskName arrayname
taskName="$1"
shift
taskType="$(type -t $taskName)"
[[ "$taskType" != "function" ]] && tlib_error_exit "task($taskName) is not a function"
if ! tlib_task_is_defined "$taskName"; then
tlib_registered_tasks+=("$taskName")
arrayname="$(tlib_get_dependency_array_name "$taskName")"
eval "$arrayname=()"
tlib_debug "Registered task $taskName"
else
tlib_error_exit "Task($taskName) already defined"
fi
tlib_register_dependency "$taskName" "$@"
}
tlib_generate_task() {
local taskName taskType
taskName="$1"
shift
taskType="$(type -t "$taskName")"
[[ -n "$taskType" ]] && tlib_error_exit "task($taskName) is already defined as $taskType... cannot override"
. /dev/stdin <<EOF
$taskName() {
$(cat /dev/stdin)
}
EOF
task "$taskName" "$@"
}
phony() {
tlib_generate_task "$@" <<FUNC
:
FUNC
}
run_task() {
tlib_assert_task_is_defined "$1"
local task_list task output bgPid has_errored ret dirstackdepth
# shellcheck disable=SC2207
task_list=( $(tlib_get_dependencies_recursive "$1") )
task_list+=("$1")
tlib_debug "Run list: ${task_list[*]}"
for task in "${task_list[@]}"; do
tlib_assert_task_is_defined "$task"
done
has_errored=false
for task in "${task_list[@]}"; do
tlib_debug "processing task $task"
if $has_errored; then
tlib_debug "skipping task $task"
echo -e "TASK: $task\\r\\t\\t\\t\\t\\t[SKIPPED]"
else
pushd "$TLIB_CURRENT_DIR" > /dev/null
dirstackdepth="${#DIRSTACK[@]}"
echo -ne "TASK: $task\\r\\t\\t\\t\\t\\t[RUNNING]"
$task >&10 2>&1 < /dev/null
ret=$?
if [[ "${#DIRSTACK[@]}" -gt "$dirstackdepth" ]]; then
tlib_debug "Directory stack depth mismatch: Expected $dirstackdepth, Actual ${#DIRSTACK[@]}"
fi
while [[ "${#DIRSTACK[@]}" -ge "$dirstackdepth" ]]; do
popd > /dev/null
done
tput el1
if [[ "$ret" -ne 0 ]]; then
tlib_debug "task status $task: error($ret)"
echo -e "\\rTASK: $task\\r\\t\\t\\t\\t\\t[ERROR]"
has_errored=true
else
tlib_debug "task status $task: success"
echo -e "\\rTASK: $task\\r\\t\\t\\t\\t\\t[DONE]"
fi
fi
done
}
tlib_initialize