From 32a585d877966c6dc4e23d092a8c94ed8f821e77 Mon Sep 17 00:00:00 2001
From: ~byakuren
Date: Wed, 10 Dec 2025 03:56:48 +0000
Subject: [PATCH] Add SIGINFO handler for BSD systems (Ctrl+T status)

On BSD systems (FreeBSD, OpenBSD, macOS), pressing Ctrl+T sends SIGINFO
to show process status. This adds a handler that displays:

- Execution state (running/idle/reentrant)
- Current verb name being executed
- Script file and line number
- Session uptime
- Memory usage (with limit if set via 9!:21, and high water mark)
- Stack depth
- Thread count (if multithreaded)
- Pending interrupt indicator ([ATTN]/[BREAK])

Output follows BSD conventions (compact, single-line, program-prefixed):
  jconsole: running myfunc in /tmp/test.ijs:5, 2m30s 1.5G/4.0G (depth 3)
  jconsole: idle, 45.2s 512.0K hwm:2.1G (depth 0)

The handler is async-signal-safe, using write() instead of printf().
---
 jsrc/jconsole.c | 181 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 181 insertions(+)

diff --git a/jsrc/jconsole.c b/jsrc/jconsole.c
index d247c7f8..3cd5072b 100644
--- a/jsrc/jconsole.c
+++ b/jsrc/jconsole.c
@@ -51,6 +51,184 @@ static int norl=0;         /* disable readline/linenoise */
 static int noel=0;         /* disable readline but not linenoise */
 static void sigint(int k){jeinterrupt();signal(SIGINT,sigint);}
 static void sigint2(int k){jeinterrupt();}
+
+#if defined(SIGINFO)
+#include <sys/time.h>
+// Helper to format memory size as human-readable (writes to buf, returns chars written)
+static int siginfo_fmtmem(char *buf, int bufsz, I bytes){
+ if(bytes<0) return 0;
+ if(bytes>=1073741824) return snprintf(buf,bufsz,"%.1fG",(double)bytes/1073741824.0);
+ if(bytes>=1048576) return snprintf(buf,bufsz,"%.1fM",(double)bytes/1048576.0);
+ if(bytes>=1024) return snprintf(buf,bufsz,"%.1fK",(double)bytes/1024.0);
+ if(bytes>0) return snprintf(buf,bufsz,"%lldB",(long long)bytes);
+ return 0;
+}
+
+// SIGINFO handler for BSD systems (Ctrl+T) - shows current execution status
+// Output format follows BSD conventions (compact, program-prefixed)
+static void siginfo_handler(int k){
+ char buf[512];
+ char timebuf[32];
+ char membuf[64];
+ int len;
+ extern JST* jt;  // defined below
+ if(!jt){
+  const char msg[]="jconsole: not initialized\n";
+  (void)!write(STDERR_FILENO, msg, sizeof(msg)-1);
+  signal(SIGINFO,siginfo_handler);
+  return;
+ }
+ JS jst=JorJJTOJ(jt);
+ JJ jm=MTHREAD(jst);
+
+ // Check for pending break/interrupt
+ C brk=jst->adbreak?jst->adbreak[0]:0;
+ const char *brkstr=(brk>=2)?"[BREAK] ":(brk==1)?"[ATTN] ":"";
+
+ // Get state
+ const char *state;
+ switch(jm->recurstate){
+  case RECSTATERUNNING: state="running"; break;
+  case RECSTATERENT: state="reentrant"; break;
+  case RECSTATEPROMPTING: state="idle"; break;
+  default: state=""; break;
+ }
+
+ // Get verb name if executing
+ const char *verb=NULL;
+ int verblen=0;
+ A curname=jm->curname;
+ if(curname && (AT(curname)&NAME)){
+  NM *nmv=NAV(curname);
+  verb=nmv->s;
+  verblen=nmv->m;
+ }else{
+  // Try stack
+  DC d=jm->sitop;
+  while(d){
+   if(d->dctype==DCCALL && d->dca && (AT(d->dca)&NAME)){
+    NM *nmv=NAV(d->dca);
+    verb=nmv->s;
+    verblen=nmv->m;
+    break;
+   }
+   d=d->dclnk;
+  }
+ }
+
+ // Get script name and line
+ const char *script=NULL;
+ int scriptlen=0;
+ I scriptline=-1;
+ I4 slistx=jm->currslistx;
+ if(slistx>=0 && jst->slist){
+  A slist=jst->slist;
+  if(slistx<AM(slist)){
+   A *sv=AAV(slist);
+   A s=sv[slistx];
+   if(s && AT(s)&LIT){
+    script=CAV(s);
+    scriptlen=(int)AN(s);
+   }
+  }
+ }
+ // Get line number from stack
+ DC si=jm->sitop;
+ while(si){
+  if(si->dctype==DCSCRIPT && si->dcn>=0){
+   scriptline=si->dcn;
+   break;
+  }
+  si=si->dclnk;
+ }
+
+ // Count stack depth
+ si=jm->sitop;
+ int depth=0;
+ while(si && depth<1000){depth++;si=si->dclnk;}
+
+ // Calculate session uptime from tssbase (seconds since J started)
+ struct timeval tv;
+ double uptime=0;
+ if(gettimeofday(&tv,NULL)==0 && jst->tssbase>0){
+  double now=tv.tv_sec+tv.tv_usec*1e-6;
+  uptime=now-jst->tssbase;
+ }
+ // Format uptime as Xm Ys or Xs
+ if(uptime>=60){
+  int mins=(int)(uptime/60);
+  int secs=(int)uptime%60;
+  snprintf(timebuf,sizeof(timebuf),"%dm%02ds",mins,secs);
+ }else if(uptime>0){
+  snprintf(timebuf,sizeof(timebuf),"%.1fs",uptime);
+ }else{
+  timebuf[0]=0;
+ }
+
+ // Get memory stats
+ I memused=jm->malloctotal + jm->malloctotalremote;
+ I memhwm=jm->malloctotalhwmk;  // high water mark
+ I memlimit=jst->mmax;  // allocation limit (0 = unlimited)
+
+ // Format memory: "used" or "used/limit" if limit explicitly set, plus "hwm:X" if different from used
+ // Default mmax is 1<<62 (effectively unlimited), only show if smaller
+ char usedbuf[16], hwmbuf[24], limitbuf[16];
+ int pos=0;
+ I defaultlimit=(I)1<<62;  // MLEN-1 on 64-bit
+ if(memused>=0 && siginfo_fmtmem(usedbuf,sizeof(usedbuf),memused)>0){
+  pos=snprintf(membuf,sizeof(membuf),"%s",usedbuf);
+  // Add limit only if explicitly set (smaller than default)
+  if(memlimit>0 && memlimit<defaultlimit && siginfo_fmtmem(limitbuf,sizeof(limitbuf),memlimit)>0){
+   pos+=snprintf(membuf+pos,sizeof(membuf)-pos,"/%s",limitbuf);
+  }
+  // Add high water mark if significantly different from current (>10% more)
+  if(memhwm>memused && memhwm>memused+(memused/10) && siginfo_fmtmem(hwmbuf,sizeof(hwmbuf),memhwm)>0){
+   pos+=snprintf(membuf+pos,sizeof(membuf)-pos," hwm:%s",hwmbuf);
+  }
+ }else{
+  membuf[0]=0;
+ }
+
+ // Get thread count (master + workers)
+ int nthreads=1+jst->wthreadhwmk;
+
+ // Build suffix: (depth N) or (depth N, M threads)
+ char suffixbuf[48];
+ if(nthreads>1){
+  snprintf(suffixbuf,sizeof(suffixbuf),"(depth %d, %d threads)",depth,nthreads);
+ }else{
+  snprintf(suffixbuf,sizeof(suffixbuf),"(depth %d)",depth);
+ }
+
+ // Format output line (BSD style: program: info)
+ // Format: jconsole: [BREAK] state verb in script:line, time mem (depth D[, N threads])
+ if(verb && script && scriptline>=0){
+  len=snprintf(buf,sizeof(buf),"jconsole: %s%s %.*s in %.*s:%lld, %s %s %s\n",
+   brkstr,state,verblen,verb,scriptlen,script,(long long)scriptline,timebuf,membuf,suffixbuf);
+ }else if(verb && script){
+  len=snprintf(buf,sizeof(buf),"jconsole: %s%s %.*s in %.*s, %s %s %s\n",
+   brkstr,state,verblen,verb,scriptlen,script,timebuf,membuf,suffixbuf);
+ }else if(verb){
+  len=snprintf(buf,sizeof(buf),"jconsole: %s%s %.*s, %s %s %s\n",
+   brkstr,state,verblen,verb,timebuf,membuf,suffixbuf);
+ }else if(script && scriptline>=0){
+  len=snprintf(buf,sizeof(buf),"jconsole: %s%s in %.*s:%lld, %s %s %s\n",
+   brkstr,state,scriptlen,script,(long long)scriptline,timebuf,membuf,suffixbuf);
+ }else if(script){
+  len=snprintf(buf,sizeof(buf),"jconsole: %s%s in %.*s, %s %s %s\n",
+   brkstr,state,scriptlen,script,timebuf,membuf,suffixbuf);
+ }else if(timebuf[0] || membuf[0]){
+  len=snprintf(buf,sizeof(buf),"jconsole: %s%s, %s %s %s\n",
+   brkstr,*state?state:"idle",timebuf,membuf,suffixbuf);
+ }else{
+  len=snprintf(buf,sizeof(buf),"jconsole: %s%s %s\n",
+   brkstr,*state?state:"idle",suffixbuf);
+ }
+ if(len>0)(void)!write(STDERR_FILENO,buf,(size_t)len);
+
+ signal(SIGINFO,siginfo_handler);
+}
+#endif
 #if defined(_WIN32)
 static BOOL WINAPI CtrlHandler(DWORD fdwCtrlType){
  switch (fdwCtrlType){
@@ -376,6 +554,9 @@ int main(int argc, char* argv[])
   SetConsoleCtrlHandler(CtrlHandler, TRUE);
 #else
   signal(SIGINT,sigint);
+#if defined(SIGINFO)
+  signal(SIGINFO,siginfo_handler);
+#endif
 #endif
  
 #ifdef READLINE
-- 
2.51.2

