70b873ace3ffca0850aa635f7fc43c71cc7b09d5 hiram Fri May 1 14:50:19 2026 -0700 further silent the cron outputs and add a status indicator to the watch.cgi page refs #31811 diff --git src/hg/utils/otto/userRequests/ottoRequestView.cgi src/hg/utils/otto/userRequests/ottoRequestView.cgi index ce37973303e..c918d8e69af 100644 --- src/hg/utils/otto/userRequests/ottoRequestView.cgi +++ src/hg/utils/otto/userRequests/ottoRequestView.cgi @@ -1,94 +1,101 @@ #!/usr/bin/env python3 """ottoRequestView.cgi - web view of hgcentraltest.ottoRequest. Read-only display of every row in the table, plus a per-row 'reset status' control that is the only write path exposed. Access is restricted to a single IP (UCSC VPN, 128.114.198.5). Any other REMOTE_ADDR gets a 403. """ import cgi import html +import json import os import subprocess import sys +import time +import urllib.parse ALLOWED_IP = '128.114.198.5' HGDB_CONF = '/usr/local/apache/cgi-bin/hg.conf' TRASH = '/data/apache/trash' DB = 'hgcentraltest' TABLE = 'ottoRequest' +# Galaxy queue status panel - snapshot is refreshed by ottoRequestWatch.sh +# (cron, every 11 minutes), CGI just reads it. +CACHE_PATH = '/data/apache/trash/ottoRequestGalaxyStatus.json' +CACHE_TTL = 1800 # seconds; older than this -> show "stale" instead + # from README.txt in this directory STATUS_NAMES = { 0: 'received by API', 1: 'acknowledged, email sent', 2: 'galaxy job started', 3: 'galaxy done, download started', 4: 'downloaded, track files made', 5: 'symlinks ready, awaiting push', 6: 'push complete', 7: 'ERROR', 8: 'COMPLETE (final email sent)', } COLS = ['id', 'requestType', 'fromDb', 'toDb', 'email', 'comment', 'requestTime', 'status', 'buildDir', 'completeTime'] def forbidden(msg): sys.stdout.write("Status: 403 Forbidden\r\n") sys.stdout.write("Content-Type: text/plain; charset=utf-8\r\n\r\n") sys.stdout.write(msg + "\n") sys.exit(0) def checkIp(): remote = os.environ.get('REMOTE_ADDR', '') if remote != ALLOWED_IP: - forbidden(f"Access denied for {remote!r}; this page is restricted " - f"to {ALLOWED_IP}.") + forbidden(f"Access denied for {remote!r}; this page is restricted.") def unescapeMysql(s): """Reverse `hgsql -B` escaping (\\n, \\t, \\\\, \\0). One pass so \\\\n stays a literal backslash + 'n'.""" out, i, n = [], 0, len(s) while i < n: if s[i] == '\\' and i + 1 < n: c = s[i+1] if c == 'n': out.append('\n') elif c == 't': out.append('\t') elif c == '\\': out.append('\\') elif c == '0': out.append('\0') else: out.append(s[i:i+2]) i += 2 else: out.append(s[i]); i += 1 return ''.join(out) def hgsqlRun(sql): """Run sql via hgsql against DB. Returns (ok, stdout, stderr). Running under Apache the process has no ~/.hg.conf, so point hgsql at the cgi-bin hg.conf via HGDB_CONF.""" env = dict(os.environ) env['HGDB_CONF'] = HGDB_CONF env['HOME'] = TRASH - cmd = ['/cluster/bin/x86_64/hgsql', DB, '-N', '-B', '-e', sql] + cmd = ['/cluster/bin/x86_64/hgsql', '-profile=central', DB, '-N', '-B', '-e', sql] r = subprocess.run(cmd, capture_output=True, text=True, env=env) return (r.returncode == 0, r.stdout, r.stderr) def fetchRows(): sql = f"SELECT {','.join(COLS)} FROM {TABLE} ORDER BY id DESC" ok, out, err = hgsqlRun(sql) if not ok: raise RuntimeError(err.strip() or 'hgsql failed') rows = [] if out.strip(): for line in out.rstrip('\n').split('\n'): rows.append([unescapeMysql(f) for f in line.split('\t')]) return rows @@ -97,57 +104,83 @@ rid = form.getfirst('id', '') stat = form.getfirst('status', '') if not rid.isdigit(): return None, f"bad id: {rid!r}" if not stat.isdigit() or int(stat) not in STATUS_NAMES: return None, f"bad status: {stat!r}" sql = (f"UPDATE {TABLE} SET status = {int(stat)} " f"WHERE id = {int(rid)}") ok, _out, err = hgsqlRun(sql) if not ok: return None, err.strip() or 'hgsql update failed' return (f"id={rid} status set to {stat} " f"({STATUS_NAMES[int(stat)]})"), None -def renderPage(rows, info=None, error=None): +def loadGalaxyStatus(): + """Return the Galaxy queue snapshot written by ottoRequestWatch.sh + (which calls galaxyStatus.py from cron). Returns the parsed dict + with an added 'stale' flag when the file is older than CACHE_TTL, + or None if the file is missing/unreadable.""" + try: + mtime = os.path.getmtime(CACHE_PATH) + with open(CACHE_PATH) as f: + data = json.load(f) + except (OSError, ValueError): + return None + data['stale'] = (time.time() - mtime) > CACHE_TTL + return data + + +def renderPage(rows, info=None, error=None, galaxyStatus=None): sys.stdout.write("Content-Type: text/html; charset=utf-8\r\n\r\n") out = sys.stdout.write out('\n
\n') out(f'{k}={html.escape(v)}'
for k, v in STATUS_NAMES.items()))
out(f' · {len(rows)} row(s) · '
f'refresh| {c} | ') out('set status |
|---|