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/"