Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-11-09 10:47:25

0001 #!/usr/bin/env python3
0002 """segfault-nightly-notice.py — the catalog tells the swf sessions what it
0003 cannot decide by itself.
0004 
0005 The nightly catalog_sync chain's last segfault step (swf-epicprod
0006 docs/SEGFAULT_DIAGNOSIS.md, Reproduction): after the inventory and the
0007 automatic dig, one TJAI peer message to the swf sessions on this host
0008 (the host:swf-testbed group) carrying the census of the catalog and the
0009 signatures that need a decision a person or an LLM makes: the ones with
0010 no trace and a runnable row and no attempt in flight (a reproduction is
0011 the only extraction left), the traced ones no finding reads, and the
0012 ones whose reproduction has settled (the row ran clean, or crashed
0013 without a trace) that no finding reads: those are read from the record
0014 into a finding, never run again. A session takes a reproduction item
0015 by requesting it on the record (the signature page's Reproduce, or the
0016 MCP tool panda_segfault_reproduce), which every session sees on the
0017 runs page, so two never submit the same row; a reading is written with
0018 panda_segfault_finding_set. Nothing is submitted here.
0019 
0020 The sender is a registered TJAI session of its own (client epicprod,
0021 name epicprod-nightly), stable across runs. Registration and sending
0022 use the tjai MCP service over HTTPS with TJAI_MCP_TOKEN from ~/.env of
0023 the running account, the route tjrepo's scripts/mcp_call.py uses.
0024 
0025 Django-bootstrap standalone script, by hand::
0026 
0027     cd /data/wenauseic/github/swf-monitor/src
0028     ../../swf-testbed/.venv/bin/python ../scripts/segfault-nightly-notice.py [--dry-run] [--limit N]
0029 
0030 Prints one JSON line: the census, the listed keys and the message id
0031 (or the message text with --dry-run). Exit 1 when the send failed.
0032 """
0033 import argparse
0034 import json
0035 import os
0036 import socket
0037 import sys
0038 import urllib.request
0039 import uuid
0040 
0041 THIS_DIR = os.path.dirname(os.path.abspath(__file__))
0042 sys.path.insert(0, os.path.join(THIS_DIR, '..', 'src'))
0043 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'swf_monitor_project.settings')
0044 
0045 import django  # noqa: E402
0046 django.setup()
0047 
0048 from monitor_app.models import CrashSignature  # noqa: E402
0049 from monitor_app.reproductions import SETTLED_OUTCOMES, attempts  # noqa: E402
0050 from monitor_app.segfaults import covering_finding, signature_summary  # noqa: E402
0051 
0052 TJAI_MCP_URL = os.environ.get('TJAI_MCP_URL', 'https://etaverse.com/tjai/mcp/')
0053 LOCATION = os.environ.get('TJAI_LOCATION_NAME', 'swf-testbed')
0054 RESOURCE = f'host:{LOCATION}'
0055 SENDER = {'client': 'epicprod', 'name': 'epicprod-nightly',
0056           'native_id': 'epicprod-segfault-nightly-notice'}
0057 SETTLED = {'diagnosed', 'handed_off', 'fixed', 'accepted'}
0058 CATALOG_URL = 'https://epic-devcloud.org/prod/panda/segfaults/'
0059 
0060 
0061 def _token():
0062     tok = os.environ.get('TJAI_MCP_TOKEN')
0063     if tok:
0064         return tok
0065     path = os.path.expanduser('~/.env')
0066     with open(path) as fh:
0067         for line in fh:
0068             line = line.strip()
0069             if line.startswith('export '):
0070                 line = line[7:]
0071             if line.startswith('TJAI_MCP_TOKEN='):
0072                 return line.split('=', 1)[1].strip().strip('"').strip("'")
0073     raise RuntimeError(f'TJAI_MCP_TOKEN not in the environment or {path}')
0074 
0075 
0076 def mcp_call(tool, arguments, timeout=60):
0077     """One tjai MCP tool call; the text result, or RuntimeError."""
0078     body = {'jsonrpc': '2.0', 'id': 1, 'method': 'tools/call',
0079             'params': {'name': tool, 'arguments': arguments}}
0080     req = urllib.request.Request(
0081         TJAI_MCP_URL, data=json.dumps(body).encode(), method='POST',
0082         headers={'Authorization': f'Bearer {_token()}',
0083                  'Content-Type': 'application/json',
0084                  'Accept': 'application/json, text/event-stream'})
0085     with urllib.request.urlopen(req, timeout=timeout) as r:
0086         resp = json.loads(r.read().decode())
0087     if 'error' in resp:
0088         raise RuntimeError(f"{tool}: MCP error {resp['error']}")
0089     res = resp['result']
0090     text = ''.join(c.get('text', '') for c in res.get('content', []))
0091     if res.get('isError'):
0092         raise RuntimeError(f'{tool}: tool error {text[:500]}')
0093     return text
0094 
0095 
0096 def census():
0097     """The catalog read for the notice: counts and the three decision lists."""
0098     active = {}
0099     for a in attempts():
0100         if a.get('active'):
0101             active.setdefault(a['signature'], 0)
0102             active[a['signature']] += 1
0103     sigs = list(CrashSignature.objects.all())
0104     # Frame groups (level trace) gather record signatures crashing in one
0105     # frame; they are counted apart, so the categories below sum to the total.
0106     counts = {'signatures': sum(1 for s in sigs if s.level != 'trace'),
0107               'frame_groups': sum(1 for s in sigs if s.level == 'trace'),
0108               'crashes': 0, 'covered': 0, 'covered_crashes': 0,
0109               'traced_unread': 0, 'settled_unread': 0, 'untraced_runnable': 0,
0110               'untraced_unrunnable': 0, 'in_flight': 0}
0111     need_run, need_read, need_reading = [], [], []
0112     for sig in sigs:
0113         if sig.level == 'trace':
0114             continue
0115         s = signature_summary(sig)
0116         counts['crashes'] += s['crashes'] or 0
0117         # In flight is counted whatever the signature's reading: a covered
0118         # signature's verifying run is a run.
0119         if active.get(sig.key):
0120             counts['in_flight'] += 1
0121         fid = covering_finding(sig)
0122         if fid or sig.status in SETTLED:
0123             counts['covered'] += 1
0124             counts['covered_crashes'] += s['crashes'] or 0
0125             continue
0126         if active.get(sig.key):
0127             continue
0128         row = {'key': sig.key, 'class': s['class'], 'crashes': s['crashes'],
0129                'queue': ', '.join(s['site_names'][:2]), 'last_seen': (s['last_seen'] or '')[:10],
0130                'frame': s['frame'], 'trace_status': s['trace_status'],
0131                'runnable': s['runnable'], 'outcome': s.get('reproduction_outcome') or ''}
0132         if s['trace_status'] == 'found':
0133             counts['traced_unread'] += 1
0134             need_read.append(row)
0135         elif row['outcome'] in SETTLED_OUTCOMES:
0136             # The pair has run and settled it without a trace: the reading
0137             # comes from the record (the per-host and per-day shape), not
0138             # from another run of the same row.
0139             counts['settled_unread'] += 1
0140             need_reading.append(row)
0141         elif s['runnable'] or s.get('es_unit'):
0142             # An Event Service unit signature is runnable by its request on
0143             # npps0 (SEGFAULT_DIAGNOSIS.md, Event Service units).
0144             counts['untraced_runnable'] += 1
0145             need_run.append(row)
0146         else:
0147             counts['untraced_unrunnable'] += 1
0148     need_run.sort(key=lambda r: -(r['crashes'] or 0))
0149     need_read.sort(key=lambda r: -(r['crashes'] or 0))
0150     need_reading.sort(key=lambda r: -(r['crashes'] or 0))
0151     return counts, need_run, need_read, need_reading
0152 
0153 
0154 def compose(counts, need_run, need_read, need_reading, limit):
0155     c = counts
0156     lines = [
0157         f"Segfault catalog, nightly: {c['signatures']} signatures "
0158         f"(and {c['frame_groups']} frame groups over them), {c['crashes']:,} crashes; "
0159         f"{c['covered']} read by a finding ({c['covered_crashes']:,} crashes); "
0160         f"{c['in_flight']} with a reproduction in flight; "
0161         f"{c['untraced_runnable']} with no trace and a runnable row; "
0162         f"{c['traced_unread']} traced with no reading; "
0163         f"{c['settled_unread']} settled by a reproduction with no trace and no reading; "
0164         f"{c['untraced_unrunnable']} with no trace and no runnable row (manifest reconstruction).",
0165     ]
0166     if need_run:
0167         lines.append(f"Reproduction is the only extraction left for these (largest first, {min(len(need_run), limit)} of {len(need_run)}):")
0168         for r in need_run[:limit]:
0169             lines.append(f"  {r['key']}  {r['class']}  {r['crashes']:,} crashes  {r['queue']}  last {r['last_seen']}")
0170     if need_read:
0171         lines.append(f"Traced, no finding reads them ({min(len(need_read), limit)} of {len(need_read)}):")
0172         for r in need_read[:limit]:
0173             lines.append(f"  {r['key']}  {r['class']}  {r['crashes']:,} crashes  {r['frame'][:70]}")
0174     if need_reading:
0175         lines.append(
0176             f"Settled by a reproduction, no trace, no finding reads them: a reading from the record "
0177             f"(per host and per day), not another run ({min(len(need_reading), limit)} of {len(need_reading)}):")
0178         for r in need_reading[:limit]:
0179             lines.append(f"  {r['key']}  {r['class']}  {r['crashes']:,} crashes  {r['outcome']}  {r['queue']}  last {r['last_seen']}")
0180     lines.append(
0181         "Someone swf take it: a reproduction is requested on the record with panda_segfault_reproduce(key) "
0182         "or the signature page's Reproduce; the runs page shows what is already requested, so check it "
0183         f"before submitting; an es_unit signature (an Event Service unit) is replayed on npps0 with "
0184         f"scripts/segfault-es-reproduce.py --key <key>. A reading is written with "
0185         f"panda_segfault_finding_set. {CATALOG_URL}")
0186     return '\n'.join(lines)
0187 
0188 
0189 def send(text):
0190     reg = json.loads(mcp_call('register_session', dict(
0191         SENDER, host=LOCATION, cwd=os.getcwd(), delivery='pull', state='idle',
0192         model='', resources=[RESOURCE])))
0193     sender_id = reg.get('id') or reg.get('session_id')
0194     if not sender_id:
0195         raise RuntimeError(f'register_session returned no id: {str(reg)[:300]}')
0196     message_id = str(uuid.uuid4())
0197     mcp_call('send_message', {'sender_id': sender_id, 'resource': RESOURCE,
0198                               'message_id': message_id, 'content': text,
0199                               'reply_requested': False})
0200     return sender_id, message_id
0201 
0202 
0203 def main():
0204     ap = argparse.ArgumentParser(description=__doc__.split('\n\n')[0])
0205     ap.add_argument('--dry-run', action='store_true', help='compose and print, send nothing')
0206     ap.add_argument('--limit', type=int, default=12, help='signatures listed per list')
0207     args = ap.parse_args()
0208     counts, need_run, need_read, need_reading = census()
0209     text = compose(counts, need_run, need_read, need_reading, args.limit)
0210     out = {'counts': counts, 'listed_run': [r['key'] for r in need_run[:args.limit]],
0211            'listed_read': [r['key'] for r in need_read[:args.limit]],
0212            'listed_settled': [r['key'] for r in need_reading[:args.limit]],
0213            'host': socket.gethostname()}
0214     if args.dry_run:
0215         out['message'] = text
0216         print(json.dumps(out))
0217         return 0
0218     try:
0219         sender_id, message_id = send(text)
0220     except Exception as exc:                                  # noqa: BLE001
0221         out['error'] = str(exc)[:500]
0222         print(json.dumps(out))
0223         return 1
0224     out.update(sender_id=sender_id, message_id=message_id, resource=RESOURCE)
0225     print(json.dumps(out))
0226     return 0
0227 
0228 
0229 if __name__ == '__main__':
0230     sys.exit(main())