summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-05 16:22:32 +0300
committerPaul Buetow <paul@buetow.org>2026-06-05 16:22:32 +0300
commit3d3c5fda1021206655783b101e85f0b41f3588f4 (patch)
tree61c1048d91c6ed6e8bd5e97c786db7411b4b921a
parent41aac9fb4aed365b8f0fc8ec09798268da5f036c (diff)
Add timeouts for generation tools
-rw-r--r--README.md14
-rwxr-xr-xbin/photoalbum54
-rw-r--r--src/photoalbum.default.conf4
-rwxr-xr-xsrc/photoalbum.sh54
-rwxr-xr-xtests/cli.sh88
-rwxr-xr-xtests/helpers.sh29
6 files changed, 224 insertions, 19 deletions
diff --git a/README.md b/README.md
index 3409a02..5df5d71 100644
--- a/README.md
+++ b/README.md
@@ -68,8 +68,8 @@ If the file is missing, run `photoalbum --init` first.
The config file is a Bash file with assignments such as `INCOMING_DIR`,
`DIST_DIR`, `TEMPLATE_DIR`, `TITLE`, `HEIGHT`, `THUMBHEIGHT`, `MAXPREVIEWS`,
-`IMAGE_JOBS`, `RANDOM_SEED`, `SHUFFLE`, `SPLASH_PAGE`, and
-`TARBALL_INCLUDE`.
+`IMAGE_JOBS`, `IMAGEMAGICK_TIMEOUT`, `RANDOM_SEED`, `SHUFFLE`, `SPLASH_PAGE`,
+`TARBALL_INCLUDE`, and `TAR_TIMEOUT`.
Before generating, `photoalbum` validates the loaded config and command-line
overrides. It checks required values, positive integer settings, `yes`/`no`
@@ -88,9 +88,9 @@ tarball filename uses `<timestamp>` as a placeholder so the output is stable.
`--print-config` writes stable shell-style assignments to stdout in this order:
`CONFIG_SOURCE`, `INCOMING_DIR`, `DIST_DIR`, `TEMPLATE_DIR`, `TITLE`, `HEIGHT`,
-`THUMBHEIGHT`, `MAXPREVIEWS`, `IMAGE_JOBS`, `RANDOM_SEED`, `SHUFFLE`,
-`SPLASH_PAGE`,
-`TARBALL_INCLUDE`, `TARBALL_SUFFIX`, `TAR_OPTS`, and `ORIGINAL_BASEPATH`.
+`THUMBHEIGHT`, `MAXPREVIEWS`, `IMAGE_JOBS`, `IMAGEMAGICK_TIMEOUT`,
+`RANDOM_SEED`, `SHUFFLE`, `SPLASH_PAGE`, `TARBALL_INCLUDE`,
+`TARBALL_SUFFIX`, `TAR_TIMEOUT`, `TAR_OPTS`, and `ORIGINAL_BASEPATH`.
Scalar values use Bash `%q` quoting and `TAR_OPTS` is normalized to a Bash array
assignment, so the output can be parsed by shell tooling. `--quiet` does not
suppress this output, and `--verbose` does not add human-readable diagnostics to
@@ -152,6 +152,10 @@ ImageMagick photo processing runs in parallel. The default is `IMAGE_JOBS=3`.
Set `IMAGE_JOBS` in the config, or pass `--image-jobs N`, to tune the number of
concurrent ImageMagick jobs.
+Each ImageMagick command is bounded by `IMAGEMAGICK_TIMEOUT=60` seconds, and
+tarball creation is bounded by `TAR_TIMEOUT=120` seconds. Set either config
+value to a positive integer to adjust the limit for large images or archives.
+
## Example usage
1. Run `photoalbum --init`.
diff --git a/bin/photoalbum b/bin/photoalbum
index 172309f..8f7f1bd 100755
--- a/bin/photoalbum
+++ b/bin/photoalbum
@@ -248,9 +248,9 @@ init_config() {
imagemagick() {
if command -v magick >/dev/null 2>&1; then
- magick "$@"
+ run_with_timeout ImageMagick "$IMAGEMAGICK_TIMEOUT" magick "$@"
elif command -v convert >/dev/null 2>&1; then
- convert "$@"
+ run_with_timeout ImageMagick "$IMAGEMAGICK_TIMEOUT" convert "$@"
else
printf 'ERROR: ImageMagick is required; install magick or convert\n' >&2
return 127
@@ -259,14 +259,18 @@ imagemagick() {
imagemagick_identify() {
if command -v magick >/dev/null 2>&1; then
- magick identify "$@"
+ run_with_timeout ImageMagick "$IMAGEMAGICK_TIMEOUT" \
+ magick identify "$@"
elif command -v identify >/dev/null 2>&1; then
- identify "$@"
+ run_with_timeout ImageMagick "$IMAGEMAGICK_TIMEOUT" \
+ identify "$@"
elif command -v convert >/dev/null 2>&1; then
if [[ "${1:-}" = '-verbose' && $# -eq 2 ]]; then
- convert "$2" -verbose info:
+ run_with_timeout ImageMagick "$IMAGEMAGICK_TIMEOUT" \
+ convert "$2" -verbose info:
else
- convert "$@" info:
+ run_with_timeout ImageMagick "$IMAGEMAGICK_TIMEOUT" \
+ convert "$@" info:
fi
else
printf 'ERROR: ImageMagick is required; install magick or convert\n' >&2
@@ -274,6 +278,31 @@ imagemagick_identify() {
fi
}
+run_with_timeout() {
+ local -r description="$1"; shift
+ local -r seconds="$1"; shift
+ local -i status=0
+
+ if ! command -v timeout >/dev/null 2>&1; then
+ printf 'ERROR: timeout command is required to run %s\n' \
+ "$description" >&2
+ return 127
+ fi
+
+ if timeout "$seconds" "$@"; then
+ return
+ else
+ status=$?
+ fi
+
+ if (( status == 124 )); then
+ printf 'ERROR: %s timed out after %s seconds\n' \
+ "$description" "$seconds" >&2
+ fi
+
+ return "$status"
+}
+
tarball() {
local -r tarball_name="$1"; shift
local base
@@ -288,7 +317,8 @@ tarball() {
"from $INCOMING_DIR"
(
cd "$(dirname "$INCOMING_DIR")"
- tar "${tar_opts[@]}" -f "$DIST_DIR/$tarball_name" "$base"
+ run_with_timeout tar "$TAR_TIMEOUT" \
+ tar "${tar_opts[@]}" -f "$DIST_DIR/$tarball_name" "$base"
)
}
@@ -1870,11 +1900,13 @@ print_config() {
print_shell_assignment THUMBHEIGHT "$THUMBHEIGHT"
print_shell_assignment MAXPREVIEWS "$MAXPREVIEWS"
print_shell_assignment IMAGE_JOBS "$IMAGE_JOBS"
+ print_shell_assignment IMAGEMAGICK_TIMEOUT "$IMAGEMAGICK_TIMEOUT"
print_shell_assignment RANDOM_SEED "${RANDOM_SEED:-}"
print_shell_assignment SHUFFLE "${SHUFFLE:-no}"
print_shell_assignment SPLASH_PAGE "${SPLASH_PAGE:-yes}"
print_shell_assignment TARBALL_INCLUDE "${TARBALL_INCLUDE:-no}"
print_shell_assignment TARBALL_SUFFIX "${TARBALL_SUFFIX:-.tar}"
+ print_shell_assignment TAR_TIMEOUT "$TAR_TIMEOUT"
print_shell_array_assignment TAR_OPTS "${tar_opts[@]}"
print_shell_assignment ORIGINAL_BASEPATH "${ORIGINAL_BASEPATH:-}"
}
@@ -2065,12 +2097,14 @@ missing_config() {
apply_config_defaults() {
HEIGHT="${HEIGHT:-}"
IMAGE_JOBS="${IMAGE_JOBS:-3}"
+ IMAGEMAGICK_TIMEOUT="${IMAGEMAGICK_TIMEOUT:-60}"
ORIGINAL_BASEPATH="${ORIGINAL_BASEPATH:-}"
RANDOM_SEED="${RANDOM_SEED:-}"
SHUFFLE="${SHUFFLE:-no}"
SPLASH_PAGE="${SPLASH_PAGE:-yes}"
TARBALL_INCLUDE="${TARBALL_INCLUDE:-no}"
TARBALL_SUFFIX="${TARBALL_SUFFIX:-.tar}"
+ TAR_TIMEOUT="${TAR_TIMEOUT:-120}"
if ! declare -p TAR_OPTS >/dev/null 2>&1; then
TAR_OPTS=(-c)
fi
@@ -2275,6 +2309,8 @@ validate_generation_config() {
validate_positive_integer_config_var THUMBHEIGHT
validate_positive_integer_config_var MAXPREVIEWS
validate_positive_integer_config_var IMAGE_JOBS
+ validate_positive_integer_config_var IMAGEMAGICK_TIMEOUT
+ validate_positive_integer_config_var TAR_TIMEOUT
validate_yes_no_config_var SHUFFLE
validate_yes_no_config_var SPLASH_PAGE
validate_yes_no_config_var TARBALL_INCLUDE
@@ -2314,6 +2350,8 @@ validate_print_config() {
validate_positive_integer_config_var THUMBHEIGHT
validate_positive_integer_config_var MAXPREVIEWS
validate_positive_integer_config_var IMAGE_JOBS
+ validate_positive_integer_config_var IMAGEMAGICK_TIMEOUT
+ validate_positive_integer_config_var TAR_TIMEOUT
validate_yes_no_config_var SHUFFLE
validate_yes_no_config_var SPLASH_PAGE
validate_yes_no_config_var TARBALL_INCLUDE
@@ -2440,6 +2478,8 @@ log_configured_action() {
log_verbose "Effective output directory: ${DIST_DIR:-}"
log_verbose "Effective template directory: ${TEMPLATE_DIR:-}"
log_verbose "Effective image jobs: ${IMAGE_JOBS:-3}"
+ log_verbose "Effective ImageMagick timeout: ${IMAGEMAGICK_TIMEOUT:-60}s"
+ log_verbose "Effective tar timeout: ${TAR_TIMEOUT:-120}s"
log_verbose "Effective splash page setting: ${SPLASH_PAGE:-yes}"
log_verbose "Effective tarball setting: ${TARBALL_INCLUDE:-no}"
}
diff --git a/src/photoalbum.default.conf b/src/photoalbum.default.conf
index 6733f19..6ce1f6d 100644
--- a/src/photoalbum.default.conf
+++ b/src/photoalbum.default.conf
@@ -9,6 +9,8 @@ HEIGHT=1200
MAXPREVIEWS=40
# Parallel ImageMagick jobs for photo, thumbnail, and blur processing.
IMAGE_JOBS=3
+# Timeout in seconds for each ImageMagick command.
+IMAGEMAGICK_TIMEOUT=60
# Randomly shuffle all previews.
# SHUFFLE=yes
# Generate a splash landing page at index.html.
@@ -26,6 +28,8 @@ TEMPLATE_DIR=/usr/share/photoalbum/templates/default
# Includes a .tar of the incoming dir in the dist, can be yes or no
TARBALL_INCLUDE=yes
TARBALL_SUFFIX=.tar
+# Timeout in seconds for tar archive creation.
+TAR_TIMEOUT=120
TAR_OPTS=(-c)
# Some debugging options
diff --git a/src/photoalbum.sh b/src/photoalbum.sh
index 211b53c..f616af7 100755
--- a/src/photoalbum.sh
+++ b/src/photoalbum.sh
@@ -248,9 +248,9 @@ init_config() {
imagemagick() {
if command -v magick >/dev/null 2>&1; then
- magick "$@"
+ run_with_timeout ImageMagick "$IMAGEMAGICK_TIMEOUT" magick "$@"
elif command -v convert >/dev/null 2>&1; then
- convert "$@"
+ run_with_timeout ImageMagick "$IMAGEMAGICK_TIMEOUT" convert "$@"
else
printf 'ERROR: ImageMagick is required; install magick or convert\n' >&2
return 127
@@ -259,14 +259,18 @@ imagemagick() {
imagemagick_identify() {
if command -v magick >/dev/null 2>&1; then
- magick identify "$@"
+ run_with_timeout ImageMagick "$IMAGEMAGICK_TIMEOUT" \
+ magick identify "$@"
elif command -v identify >/dev/null 2>&1; then
- identify "$@"
+ run_with_timeout ImageMagick "$IMAGEMAGICK_TIMEOUT" \
+ identify "$@"
elif command -v convert >/dev/null 2>&1; then
if [[ "${1:-}" = '-verbose' && $# -eq 2 ]]; then
- convert "$2" -verbose info:
+ run_with_timeout ImageMagick "$IMAGEMAGICK_TIMEOUT" \
+ convert "$2" -verbose info:
else
- convert "$@" info:
+ run_with_timeout ImageMagick "$IMAGEMAGICK_TIMEOUT" \
+ convert "$@" info:
fi
else
printf 'ERROR: ImageMagick is required; install magick or convert\n' >&2
@@ -274,6 +278,31 @@ imagemagick_identify() {
fi
}
+run_with_timeout() {
+ local -r description="$1"; shift
+ local -r seconds="$1"; shift
+ local -i status=0
+
+ if ! command -v timeout >/dev/null 2>&1; then
+ printf 'ERROR: timeout command is required to run %s\n' \
+ "$description" >&2
+ return 127
+ fi
+
+ if timeout "$seconds" "$@"; then
+ return
+ else
+ status=$?
+ fi
+
+ if (( status == 124 )); then
+ printf 'ERROR: %s timed out after %s seconds\n' \
+ "$description" "$seconds" >&2
+ fi
+
+ return "$status"
+}
+
tarball() {
local -r tarball_name="$1"; shift
local base
@@ -288,7 +317,8 @@ tarball() {
"from $INCOMING_DIR"
(
cd "$(dirname "$INCOMING_DIR")"
- tar "${tar_opts[@]}" -f "$DIST_DIR/$tarball_name" "$base"
+ run_with_timeout tar "$TAR_TIMEOUT" \
+ tar "${tar_opts[@]}" -f "$DIST_DIR/$tarball_name" "$base"
)
}
@@ -1870,11 +1900,13 @@ print_config() {
print_shell_assignment THUMBHEIGHT "$THUMBHEIGHT"
print_shell_assignment MAXPREVIEWS "$MAXPREVIEWS"
print_shell_assignment IMAGE_JOBS "$IMAGE_JOBS"
+ print_shell_assignment IMAGEMAGICK_TIMEOUT "$IMAGEMAGICK_TIMEOUT"
print_shell_assignment RANDOM_SEED "${RANDOM_SEED:-}"
print_shell_assignment SHUFFLE "${SHUFFLE:-no}"
print_shell_assignment SPLASH_PAGE "${SPLASH_PAGE:-yes}"
print_shell_assignment TARBALL_INCLUDE "${TARBALL_INCLUDE:-no}"
print_shell_assignment TARBALL_SUFFIX "${TARBALL_SUFFIX:-.tar}"
+ print_shell_assignment TAR_TIMEOUT "$TAR_TIMEOUT"
print_shell_array_assignment TAR_OPTS "${tar_opts[@]}"
print_shell_assignment ORIGINAL_BASEPATH "${ORIGINAL_BASEPATH:-}"
}
@@ -2065,12 +2097,14 @@ missing_config() {
apply_config_defaults() {
HEIGHT="${HEIGHT:-}"
IMAGE_JOBS="${IMAGE_JOBS:-3}"
+ IMAGEMAGICK_TIMEOUT="${IMAGEMAGICK_TIMEOUT:-60}"
ORIGINAL_BASEPATH="${ORIGINAL_BASEPATH:-}"
RANDOM_SEED="${RANDOM_SEED:-}"
SHUFFLE="${SHUFFLE:-no}"
SPLASH_PAGE="${SPLASH_PAGE:-yes}"
TARBALL_INCLUDE="${TARBALL_INCLUDE:-no}"
TARBALL_SUFFIX="${TARBALL_SUFFIX:-.tar}"
+ TAR_TIMEOUT="${TAR_TIMEOUT:-120}"
if ! declare -p TAR_OPTS >/dev/null 2>&1; then
TAR_OPTS=(-c)
fi
@@ -2275,6 +2309,8 @@ validate_generation_config() {
validate_positive_integer_config_var THUMBHEIGHT
validate_positive_integer_config_var MAXPREVIEWS
validate_positive_integer_config_var IMAGE_JOBS
+ validate_positive_integer_config_var IMAGEMAGICK_TIMEOUT
+ validate_positive_integer_config_var TAR_TIMEOUT
validate_yes_no_config_var SHUFFLE
validate_yes_no_config_var SPLASH_PAGE
validate_yes_no_config_var TARBALL_INCLUDE
@@ -2314,6 +2350,8 @@ validate_print_config() {
validate_positive_integer_config_var THUMBHEIGHT
validate_positive_integer_config_var MAXPREVIEWS
validate_positive_integer_config_var IMAGE_JOBS
+ validate_positive_integer_config_var IMAGEMAGICK_TIMEOUT
+ validate_positive_integer_config_var TAR_TIMEOUT
validate_yes_no_config_var SHUFFLE
validate_yes_no_config_var SPLASH_PAGE
validate_yes_no_config_var TARBALL_INCLUDE
@@ -2440,6 +2478,8 @@ log_configured_action() {
log_verbose "Effective output directory: ${DIST_DIR:-}"
log_verbose "Effective template directory: ${TEMPLATE_DIR:-}"
log_verbose "Effective image jobs: ${IMAGE_JOBS:-3}"
+ log_verbose "Effective ImageMagick timeout: ${IMAGEMAGICK_TIMEOUT:-60}s"
+ log_verbose "Effective tar timeout: ${TAR_TIMEOUT:-120}s"
log_verbose "Effective splash page setting: ${SPLASH_PAGE:-yes}"
log_verbose "Effective tarball setting: ${TARBALL_INCLUDE:-no}"
}
diff --git a/tests/cli.sh b/tests/cli.sh
index e4a94a6..367cd1a 100755
--- a/tests/cli.sh
+++ b/tests/cli.sh
@@ -927,11 +927,13 @@ HEIGHT=1200
THUMBHEIGHT=300
MAXPREVIEWS=40
IMAGE_JOBS=3
+IMAGEMAGICK_TIMEOUT=60
RANDOM_SEED=''
SHUFFLE=no
SPLASH_PAGE=yes
TARBALL_INCLUDE=yes
TARBALL_SUFFIX=.tar
+TAR_TIMEOUT=120
TAR_OPTS=( -c )
ORIGINAL_BASEPATH=''
EOF
@@ -1062,11 +1064,13 @@ HEIGHT=120
THUMBHEIGHT=30
MAXPREVIEWS=7
IMAGE_JOBS=3
+IMAGEMAGICK_TIMEOUT=60
RANDOM_SEED=''
SHUFFLE=yes
SPLASH_PAGE=yes
TARBALL_INCLUDE=no
TARBALL_SUFFIX=.tar
+TAR_TIMEOUT=120
TAR_OPTS=( -c )
ORIGINAL_BASEPATH=''
EOF
@@ -1099,11 +1103,13 @@ HEIGHT=120
THUMBHEIGHT=30
MAXPREVIEWS=8
IMAGE_JOBS=3
+IMAGEMAGICK_TIMEOUT=60
RANDOM_SEED=''
SHUFFLE=no
SPLASH_PAGE=yes
TARBALL_INCLUDE=no
TARBALL_SUFFIX=.tar
+TAR_TIMEOUT=120
TAR_OPTS=( -c )
ORIGINAL_BASEPATH=''
EOF
@@ -1161,11 +1167,13 @@ HEIGHT=456
THUMBHEIGHT=45
MAXPREVIEWS=9
IMAGE_JOBS=2
+IMAGEMAGICK_TIMEOUT=60
RANDOM_SEED=cli-seed
SHUFFLE=yes
SPLASH_PAGE=no
TARBALL_INCLUDE=yes
TARBALL_SUFFIX=.tar
+TAR_TIMEOUT=120
TAR_OPTS=( -c )
ORIGINAL_BASEPATH=''
EOF
@@ -2205,6 +2213,80 @@ test_generate_imagemagick_failure_preserves_dist() {
test::teardown
}
+test_generate_imagemagick_timeout_preserves_dist() {
+ local config_file
+ local fake_bin
+ local output
+
+ test::setup
+ fake_bin="$TEST_TMPDIR/bin"
+ config_file="$TEST_TMPDIR/photoalbum.conf"
+
+ test::install_hanging_imagemagick "$fake_bin"
+ mkdir -p "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist"
+ printf 'fake image\n' > "$TEST_TMPDIR/incoming/01.jpg"
+ printf 'old dist\n' > "$TEST_TMPDIR/dist/index.html"
+ printf 'keep me\n' > "$TEST_TMPDIR/dist/sentinel"
+ test::write_album_config \
+ "$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
+ 'Hanging ImageMagick album' 40
+ printf 'IMAGEMAGICK_TIMEOUT=1\n' >> "$config_file"
+
+ output=$(
+ cd "$TEST_TMPDIR"
+ PATH="$fake_bin:$PATH" TEST_HANG_SECONDS=2 \
+ test::capture_failure_output "$TEST_PHOTOALBUM" --generate
+ )
+
+ test::assert_contains \
+ 'ERROR: ImageMagick timed out after 1 seconds' "$output"
+ test "$(<"$TEST_TMPDIR/dist/index.html")" = 'old dist'
+ test "$(<"$TEST_TMPDIR/dist/sentinel")" = 'keep me'
+ test::assert_path_absent "$TEST_TMPDIR/dist/photos/01.jpg"
+ test::assert_path_absent "$TEST_TMPDIR/dist/photoalbum.json"
+ test::assert_no_staging_dirs "$TEST_TMPDIR"
+ test::teardown
+}
+
+test_generate_tar_timeout_preserves_dist() {
+ local config_file
+ local fake_bin
+ local output
+
+ test::setup
+ fake_bin="$TEST_TMPDIR/bin"
+ config_file="$TEST_TMPDIR/photoalbum.conf"
+
+ test::install_fake_imagemagick "$fake_bin"
+ PATH="$fake_bin:$PATH" \
+ test::generate_fixture_images "$TEST_TMPDIR/incoming"
+ test::install_hanging_tar "$fake_bin"
+ mkdir -p "$TEST_TMPDIR/dist"
+ printf 'old dist\n' > "$TEST_TMPDIR/dist/index.html"
+ printf 'keep me\n' > "$TEST_TMPDIR/dist/sentinel"
+ test::write_album_config \
+ "$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
+ 'Hanging tar album' 40
+ {
+ printf 'TARBALL_INCLUDE=yes\n'
+ printf 'TAR_TIMEOUT=1\n'
+ } >> "$config_file"
+
+ output=$(
+ cd "$TEST_TMPDIR"
+ PATH="$fake_bin:$PATH" TEST_HANG_SECONDS=2 \
+ test::capture_failure_output "$TEST_PHOTOALBUM" --generate
+ )
+
+ test::assert_contains 'ERROR: tar timed out after 1 seconds' "$output"
+ test "$(<"$TEST_TMPDIR/dist/index.html")" = 'old dist'
+ test "$(<"$TEST_TMPDIR/dist/sentinel")" = 'keep me'
+ test::assert_path_absent "$TEST_TMPDIR/dist/photoalbum.json"
+ test::assert_find_count 0 "$TEST_TMPDIR/dist" '*.tar'
+ test::assert_no_staging_dirs "$TEST_TMPDIR"
+ test::teardown
+}
+
test_generate_template_failure_preserves_dist() {
local config_file
local fake_bin
@@ -2867,6 +2949,12 @@ main() {
'--generate ImageMagick failure preserves final dist' \
test_generate_imagemagick_failure_preserves_dist
test::run_case \
+ '--generate ImageMagick timeout preserves final dist' \
+ test_generate_imagemagick_timeout_preserves_dist
+ test::run_case \
+ '--generate tar timeout preserves final dist' \
+ test_generate_tar_timeout_preserves_dist
+ test::run_case \
'--generate template failure preserves final dist' \
test_generate_template_failure_preserves_dist
test::run_case \
diff --git a/tests/helpers.sh b/tests/helpers.sh
index ca313fd..bf6000e 100755
--- a/tests/helpers.sh
+++ b/tests/helpers.sh
@@ -288,6 +288,21 @@ MAGICK
cp "$bin_dir/magick" "$bin_dir/convert"
}
+test::install_hanging_imagemagick() {
+ local -r bin_dir="$1"; shift
+
+ mkdir -p "$bin_dir"
+
+ cat > "$bin_dir/magick" <<'MAGICK'
+#!/usr/bin/env bash
+set -euo pipefail
+
+sleep "${TEST_HANG_SECONDS:-5}"
+MAGICK
+ chmod 0755 "$bin_dir/magick"
+ cp "$bin_dir/magick" "$bin_dir/convert"
+}
+
test::install_failing_generation_tools() {
local -r bin_dir="$1"; shift
local name
@@ -392,6 +407,20 @@ TAR
chmod 0755 "$bin_dir/tar"
}
+test::install_hanging_tar() {
+ local -r bin_dir="$1"; shift
+
+ mkdir -p "$bin_dir"
+
+ cat > "$bin_dir/tar" <<'TAR'
+#!/usr/bin/env bash
+set -euo pipefail
+
+sleep "${TEST_HANG_SECONDS:-5}"
+TAR
+ chmod 0755 "$bin_dir/tar"
+}
+
test::install_coreutils_without_imagemagick() {
local -r bin_dir="$1"; shift
local command_path