87aafb03635a2e1ea4821bb1a2e41b640c867799
hiram
  Mon Jun 8 13:11:17 2026 -0700
initial script for assembly watch refs #31811

diff --git src/hg/utils/otto/userRequests/asmRequestWatch.sh src/hg/utils/otto/userRequests/asmRequestWatch.sh
new file mode 100755
index 00000000000..ae2769498fd
--- /dev/null
+++ src/hg/utils/otto/userRequests/asmRequestWatch.sh
@@ -0,0 +1,133 @@
+#!/bin/bash
+
+# asmRequestWatch.sh - what for assembly requests in the ottoRequest table
+#
+# Initial function is just to watch the status, and when it
+#   reaches status 6 'push complete' (which currently is set manually)
+#   then send email and mark finished
+#
+# The 'otto' user cron job 'ottoRequest.py' will be watching for status 0
+#   for a new entry.  It will set status 1 and send notification email
+#
+# Different meanings from the liftOver status settings:
+#   0 pending, 1 notified, 2 in progress, 6 push is done and is available on the RR,
+#      status 7 for problems, and 8 is final notification has been sent == process is complete
+### cron job entry: (1 minute later than ottoRequestWatch.sh)
+# 10,21,32,43,54 * * * * ~/kent/src/hg/utils/otto/userRequests/asmRequestWatch.sh
+
+set -eEu -o pipefail
+umask 002
+
+export scriptDir=$(cd "$(dirname "$0")" && pwd)
+export centDb="hgcentral"
+export hgSql="hgsql -hgenome-centdb"
+
+##############################################################################
+### singleton lock - only one instance at a time
+### Open lockPath on FD 9 for the lifetime of the shell, then take a
+### non-blocking exclusive lock.  Kernel releases the lock on exit
+### (normal, error, or kill -9), so no stale lock cleanup is needed.
+### Exit 0 silently if another instance holds the lock so cron doesn't
+### email on every overlapping tick.  PID is written to the file for
+### information only see the holder via:
+###   cat asmRequestWatch.lock      (the PID)
+###   lsof asmRequestWatch.lock     (the locking process)
+##############################################################################
+export lockPath="${scriptDir}/asmRequestWatch.lock"
+# 9<> opens read+write without truncating, so a second instance that
+# comes along while we're running won't wipe our PID from the file
+# before its flock attempt fails.
+exec 9<>"${lockPath}"
+flock -n 9 || exit 0
+# we own the lock now safe to truncate and write our PID.  ': >file'
+# truncates via a separate FD; FD 9 keeps its position 0 from <>, so
+# the printf below starts writing at the beginning of the empty file.
+: >"${lockPath}"
+printf "%d\n" "$$" >&9
+##############################################################################
+
+##############################################################################
+### errors - set error status in the table
+function setErrorStatus() {
+  id="${1}"
+  /cluster/bin/x86_64/${hgSql} -N -e \
+      "UPDATE ottoRequest SET status=7 WHERE id=${id};" "${centDb}"
+}
+##############################################################################
+
+##############################################################################
+### sendNotification - email the requesting user that their assembly request is done
+###   args: reqId subject
+###   message body is read from stdin
+###   recipient: email column of ottoRequest table for that reqId
+###   bcc: genark-request-group@ucsc.edu
+###   envelope sender / Return-Path / bounce: genome-www@soe.ucsc.edu
+###   returns 0 on success, non-zero on failure
+##############################################################################
+function sendNotification() {
+  local reqId="${1}"
+  local subject="${2}"
+  local msgBody="${3}"
+  local toAddr
+  toAddr="$(/cluster/bin/x86_64/${hgSql} -N -B -e \
+    "SELECT email FROM ottoRequest WHERE id = ${reqId};" ${centDb})"
+  if [ -z "${toAddr}" ]; then
+    printf "ERROR: sendNotification: no email for request %s\n" "${reqId}" 1>&2
+    return 1
+  fi
+  local bcc="genark-request-group@ucsc.edu"
+  local from="genome-www@soe.ucsc.edu"
+  local bounce="gb"
+  bounce+="aut"
+  bounce+="o"
+  bounce+="@"
+  bounce+="uc"
+  bounce+="sc."
+  bounce+="ed"
+  bounce+="u"
+  printf "# DBG sending\n" 1>&2
+  # -f sets the envelope sender (becomes Return-Path at delivery and the
+  #    bounce address); -t reads recipients from To:/Cc:/Bcc: headers;
+  #    -oi prevents a lone "." in body from ending the message
+  {
+    printf "From: %s\n" "${from}"
+    printf "To: %s\n" "${toAddr}"
+    printf "Bcc: %s\n" "${bcc}"
+    printf "Reply-To: %s\n" "${from}"
+    printf "Subject: %s\n" "${subject}"
+    printf "\n"
+    printf "%s\n" "${msgBody}"
+  } | /usr/sbin/sendmail -f "${bounce}" -t -oi
+}	# function sendNotification()
+
+############################################################################
+# check for phase 6: the assembly is complete and available on the RR
+#       this checking and setting status 6 is currently done manually,
+#       eventually this will become automatic.
+############################################################################
+while IFS=$'\t' read -r reqId fromDb comment requestTime; do
+
+  export gcX="${fromDb:0:3}"
+  export d0="${fromDb:4:3}"
+  export d1="${fromDb:7:3}"
+  export d2="${fromDb:10:3}"
+  export gbDbPath="/gbdb/genark/${gcX}/${d0}/${d1}/${d2}/${fromDb}/hub.txt"
+  export hubTxt="https://genome.ucsc.edu/cgi-bin/hgTracks?genome=${fromDb}&hubUrl=${gbDbPath}"
+
+  sendNotification "${reqId}" \
+"from UCSC: assembly request complete: ${fromDb}" \
+"from UCSC: Your assembly request is complete:
+ assembly:     ${fromDb}
+  comment:     ${comment}
+submitted:     ${requestTime}
+
+The assembly is available in the browser at the following URL:
+  ${hubTxt}
+"
+
+/cluster/bin/x86_64/${hgSql} -N -e \
+      "UPDATE ottoRequest SET status=8, completeTime=now() WHERE id=${reqId};" ${centDb}
+
+done < <(/cluster/bin/x86_64/${hgSql} -N -B -e \
+  "SELECT id, fromDb, comment, requestTime FROM ottoRequest \
+   WHERE status = 6 AND requestType = 'assembly';" ${centDb})