798c382a4e6e6f2e9ecaf6c47300f8cc2a6e0074
braney
  Mon Aug 24 08:54:58 2026 -0700
ts: add a conf subcommand to rewrite httpd.conf without re-freezing, refs #37867

The CORS scoping fix in 4d9755a1 changed the httpd.conf template, but a
parked instance keeps the conf it was created with.  All three parked
instances therefore still set Access-Control-Allow-Origin at server
scope, including on cgi-bin.  That is the hole 4d9755a1 closed in the
template only.

The one way to pick up a template change was "ts sync", and cmd_sync
calls freeze before writeConf.  For a park whose whole point is the
frozen code, re-freezing to the current live sandbox throws away the
thing being kept.  There was no way to update only the config.

"ts conf NNNNN" rewrites httpd.conf from the current template, leaves
the frozen cgi-bin and htdocs alone, and restarts the httpd if it was
running.  Ran it on all three parked instances: cgi-bin now sends no
Access-Control header, htdocs and trash send both.

diff --git src/utils/ts/ts src/utils/ts/ts
index fce5fd87b04..8ef2b7faa2e 100755
--- src/utils/ts/ts
+++ src/utils/ts/ts
@@ -7,30 +7,31 @@
 # port, reached by an ssh tunnel (yours or a colleague's - all hgwdev users
 # share the loopback, so anyone with an hgwdev account can tunnel in).  The DB
 # and /gbdb are shared (hg.conf points at hgwdev MySQL; /gbdb is shared NFS) and
 # trash is shared with the live CGIs, so only the *code* is frozen: a full copy
 # of cgi-bin-$USER + htdocs-$USER plus a rewritten hg.conf.  Data is left
 # shared on purpose (freeze the code, share the data).
 #
 # The parked instances and the port registry live under $TS_ROOT, by default
 # $HOME/ticketSandboxes.  On hgwdev, point that at a large local pool (a freeze
 # is a few GB): mkdir /data/home/$USER/ticketSandboxes and symlink it from
 # $HOME, or set TS_ROOT.
 #
 # Subcommands (NNNNN = Redmine ticket number):
 #   create NNNNN [note]   freeze the live sandbox, start an httpd, register it
 #   sync   NNNNN          re-freeze a parked ticket to the current live sandbox
+#   conf   NNNNN          rewrite only httpd.conf, leaving the frozen code alone
 #   start  NNNNN          start the ticket's httpd (e.g. after a reboot)
 #   stop   NNNNN          stop the ticket's httpd
 #   tunnel NNNNN          open an ssh tunnel and print the browser URL
 #   list                  show all parked tickets and their running status
 #   remove NNNNN          stop the httpd and delete the ticket sandbox
 #
 set -euo pipefail
 
 # --- configuration ----------------------------------------------------------
 ROOT="$(readlink -f "${TS_ROOT:-$HOME/ticketSandboxes}")"   # parked instances live here
 REG="$ROOT/ports.tsv"                        # ticket <TAB> port <TAB> created <TAB> note
 
 TS_USER="${USER:-$(id -un)}"                 # whose live sandbox we freeze
 LIVE_CGI=/usr/local/apache/cgi-bin-$TS_USER
 LIVE_HTDOCS=/usr/local/apache/htdocs-$TS_USER
@@ -256,30 +257,49 @@
     echo "  you, remote:  ts tunnel $tkt   ->   http://localhost:$port/cgi-bin/hgTracks"
     echo "  a colleague:  ssh -N -L $port:localhost:$port <user>@$HGWDEV   ->   http://localhost:$port/cgi-bin/hgTracks"
 }
 
 cmd_sync() {
     local tkt="$1"; validTkt "$tkt"
     local port; port="$(regPort "$tkt")"
     [[ -n "$port" ]] || die "RM $tkt not found in registry"
     local running=""; [[ -n "$(pidOf "$tkt")" ]] && running=1
     [[ -n "$running" ]] && stopHttpd "$tkt"
     freeze "$tkt"
     writeConf "$tkt" "$port"     # regenerate in case template changed
     [[ -n "$running" ]] && startHttpd "$tkt" || echo "RM $tkt re-frozen (httpd was not running)."
 }
 
+# Rewrite httpd.conf from the current template without touching the frozen code.
+# "sync" also re-freezes, which is what you want after more work on the live
+# sandbox, but not when the freeze is the whole point of the park and only the
+# config template moved on.
+cmd_conf() {
+    local tkt="$1"; validTkt "$tkt"
+    local port; port="$(regPort "$tkt")"
+    [[ -n "$port" ]] || die "RM $tkt not found in registry"
+    [[ -d "$(tsDir "$tkt")" ]] || die "no sandbox directory for RM $tkt"
+    local running=""; [[ -n "$(pidOf "$tkt")" ]] && running=1
+    [[ -n "$running" ]] && stopHttpd "$tkt"
+    writeConf "$tkt" "$port"
+    if [[ -n "$running" ]]; then
+        startHttpd "$tkt"
+    else
+        echo "RM $tkt httpd.conf rewritten (httpd was not running)."
+    fi
+}
+
 cmd_start()  { validTkt "$1"; startHttpd "$1"; }
 cmd_stop()   { validTkt "$1"; stopHttpd "$1"; }
 
 cmd_port() {   # print the port for a ticket (used by the laptop-side ts wrapper)
     local tkt="$1"; validTkt "$tkt"
     local port; port="$(regPort "$tkt")"
     [[ -n "$port" ]] || die "RM $tkt not found in registry"
     echo "$port"
 }
 
 cmd_tunnel() {
     local tkt="$1"; validTkt "$tkt"
     local port; port="$(regPort "$tkt")"
     [[ -n "$port" ]] || die "RM $tkt not found in registry"
     cat <<EOF
@@ -314,24 +334,25 @@
     [[ -n "$port" ]] || die "RM $tkt not found in registry"
     stopHttpd "$tkt" || true
     rm -rf "$(tsDir "$tkt")"     # trash is a symlink: removed, not followed
     # drop the registry row
     local tmp; tmp="$(mktemp)"
     awk -F'\t' -v r="$tkt" '$1!=r' "$REG" > "$tmp" && mv "$tmp" "$REG"
     echo "RM $tkt removed."
 }
 
 # --- dispatch ----------------------------------------------------------------
 [[ $# -ge 1 ]] || usage
 sub="$1"; shift || true
 case "$sub" in
     create) [[ $# -ge 1 ]] || usage; cmd_create "$@";;
     sync)   [[ $# -eq 1 ]] || usage; cmd_sync   "$1";;
+    conf)   [[ $# -eq 1 ]] || usage; cmd_conf   "$1";;
     start)  [[ $# -eq 1 ]] || usage; cmd_start  "$1";;
     stop)   [[ $# -eq 1 ]] || usage; cmd_stop   "$1";;
     tunnel) [[ $# -eq 1 ]] || usage; cmd_tunnel "$1";;
     port)   [[ $# -eq 1 ]] || usage; cmd_port   "$1";;
     list)   cmd_list;;
     remove) [[ $# -eq 1 ]] || usage; cmd_remove "$1";;
     -h|--help|help) usage;;
-    *) die "unknown subcommand '$sub' (try: create sync start stop tunnel list remove)";;
+    *) die "unknown subcommand '$sub' (try: create sync conf start stop tunnel list remove)";;
 esac