diff --git a/docker/warm-extensions.sh b/docker/warm-extensions.sh
index e840fe9..2ca2cc4 100755
--- a/docker/warm-extensions.sh
+++ b/docker/warm-extensions.sh
@@ -20,6 +20,11 @@
set -eu
WEBROOT="$(/usr/local/bin/rustcfml-webroot)"
+# Absolute, so the link into the probe's scratch directory (below) points at it
+# from anywhere, not relative to the scratch directory.
+if [ -d "$WEBROOT" ]; then
+ WEBROOT="$(cd "$WEBROOT" && pwd)"
+fi
STRICT="${RUSTCFML_EXTENSIONS_STRICT:-1}"
BIN_DIR="$(dirname "$(readlink -f "$(command -v rustcfml)")")"
@@ -37,19 +42,61 @@ if [ "$found" -eq 0 ] && [ -z "${CFCONFIG:-}" ] && [ ! -f "$WEBROOT/.cfconfig.js
exit 0
fi
-set -- --verbose -c 'writeOutput("")'
+# The probe is an empty template in a scratch directory, not inline code run in
+# the webroot: inline code there runs the app's Application.cfc (onApplicationStart,
+# then onRequest with no page), so every container start would boot the app once
+# against its database and could log an error.
+#
+# - An empty Application.cfc beside the probe ends the engine's upward search
+# for one, so the app's is not found even when the scratch directory is under
+# the webroot (TMPDIR inside it, or a webroot of /tmp).
+# - The engine's application directory is the template's, so the webroot's
+# extensions/ is linked into the scratch directory to keep it in the search.
+# - The cwd stays the webroot, so the server .cfconfig.json (and its
+# extensions.directory) is found exactly as the server finds it.
+#
+# With no writable temp directory (a read-only container), fall back to the
+# inline probe in the webroot, which runs the app's Application.cfc as before.
+probe=""
+if probe="$(mktemp -d 2>/dev/null)"; then
+ trap 'rm -rf "$probe"' EXIT
+ : > "$probe/probe.cfm"
+ printf '\n' > "$probe/Application.cfc"
+ [ -d "$WEBROOT/extensions" ] && ln -s "$WEBROOT/extensions" "$probe/extensions"
+ set -- --verbose "$probe/probe.cfm"
+else
+ probe=""
+ echo "rustcfml: no writable temp directory; checking extensions from the webroot, which runs its Application.cfc" >&2
+ set -- --verbose -c 'writeOutput("")'
+fi
[ -n "${RUSTCFML_EXTENSIONS:-}" ] && set -- --extensions "$RUSTCFML_EXTENSIONS" "$@"
# One engine start in CLI mode walks the exact loader the server uses; verbose
-# prints "Loaded extension …" per success and "…warning…" per problem.
+# prints "Loaded extension …" per success and "…warning…" per problem. Counts
+# come from the engine's own output; only the lines shown are edited.
out="$(cd "$WEBROOT" && rustcfml "$@" 2>&1 | grep -v '^\[.*\] DEBUG ' || true)"
loaded="$(printf '%s\n' "$out" | grep -c '^Loaded extension ' || true)"
problems="$(printf '%s\n' "$out" | grep -i -c 'warning' || true)"
+# Report the scratch link's paths as the webroot's. A literal replacement (awk
+# index), so no character in either path can change what is matched.
+show() {
+ FROM="${probe:+$probe/extensions/}" TO="$WEBROOT/extensions/" awk '
+ BEGIN { f = ENVIRON["FROM"]; t = ENVIRON["TO"] }
+ {
+ rest = $0; done = ""
+ if (f != "") while ((i = index(rest, f)) > 0) {
+ done = done substr(rest, 1, i - 1) t
+ rest = substr(rest, i + length(f))
+ }
+ print " " done rest
+ }'
+}
+
if [ "$found" -gt 0 ] || [ "$loaded" -gt 0 ] || [ "$problems" -gt 0 ]; then
echo "rustcfml: extensions — $found .rcx found, $loaded loaded, $problems problem(s)"
- printf '%s\n' "$out" | grep -E '^Loaded extension |[Ww]arning' | sed 's/^/ /' || true
+ printf '%s\n' "$out" | grep -E '^Loaded extension |[Ww]arning' | show || true
fi
if [ "$problems" -gt 0 ] && [ "$STRICT" = 1 ]; then
diff --git a/tests/smoke.sh b/tests/smoke.sh
index c0105fa..db611c3 100755
--- a/tests/smoke.sh
+++ b/tests/smoke.sh
@@ -104,6 +104,59 @@ curl -s "http://127.0.0.1:$PORT/index.cfm" | grep -q "Hello from RustCFML" || fa
docker rm -f "$cid" >/dev/null; trap - EXIT
rm -rf "$tmp"
+echo "--- the extension check does not start the application"
+# The check runs before the server, at every container start. Booting the app's
+# Application.cfc there would open its database and fire its handlers with no page.
+tmp=$(mktemp -d)
+echo '{}' > "$tmp/.cfconfig.json"
+cat > "$tmp/Application.cfc" <<'CFC'
+
+
+
+
+CFC
+docker run --rm --entrypoint sh -v "$tmp:/srv/app:ro" "$IMAGE" \
+ -c 'rustcfml-warm-extensions && test ! -e /tmp/app-started' \
+ || fail "the extension check ran the application's onApplicationStart"
+rm -rf "$tmp"
+
+echo "--- ... nor when its scratch directory is inside the webroot"
+# The probe's own empty Application.cfc stops the engine's upward search there.
+tmp=$(mktemp -d); mkdir -m 0777 "$tmp/scratch"
+echo '{}' > "$tmp/.cfconfig.json"
+cat > "$tmp/Application.cfc" <<'CFC'
+
+
+
+
+CFC
+docker run --rm --entrypoint sh -v "$tmp:/srv/app" -e TMPDIR=/srv/app/scratch "$IMAGE" \
+ -c 'rustcfml-warm-extensions && test ! -e /tmp/app-started' \
+ || fail "with TMPDIR inside the webroot, the extension check ran onApplicationStart"
+rm -rf "$tmp"
+
+echo "--- a broken extension is still fatal from a relative webroot, or one with | and & in its path"
+tmp=$(mktemp -d); mkdir -p "$tmp/extensions"; echo junk > "$tmp/extensions/bad-0.0.1.rcx"
+set +e
+docker run --rm --entrypoint sh -v "$tmp:/srv/app:ro" -w /srv/app -e RUSTCFML_WEBROOT=. "$IMAGE" \
+ -c 'rustcfml-warm-extensions' >/dev/null 2>&1; rc=$?
+set -e
+[ "$rc" -eq 78 ] || fail "relative webroot: expected exit 78, got $rc"
+set +e
+out=$(docker run --rm --entrypoint sh -v "$tmp:/srv/a|b&c:ro" -e "RUSTCFML_WEBROOT=/srv/a|b&c" "$IMAGE" \
+ -c 'rustcfml-warm-extensions' 2>&1); rc=$?
+set -e
+echo "$out"
+[ "$rc" -eq 78 ] || fail "webroot with | and &: expected exit 78, got $rc"
+echo "$out" | grep -qF "/srv/a|b&c/extensions/bad-0.0.1.rcx" || fail "webroot with | and &: path not reported as the webroot's"
+rm -rf "$tmp"
+
+echo "--- a read-only container still passes the extension check"
+tmp=$(mktemp -d); echo '{}' > "$tmp/.cfconfig.json"
+docker run --rm --read-only --entrypoint sh -v "$tmp:/srv/app:ro" "$IMAGE" -c 'rustcfml-warm-extensions' \
+ || fail "the extension check failed in a read-only container"
+rm -rf "$tmp"
+
if [ -n "${SMOKE_RCX:-}" ]; then
echo "--- real extension: $SMOKE_RCX"
tmp=$(mktemp -d); mkdir -p "$tmp/extensions"; cp "$SMOKE_RCX" "$tmp/extensions/"