#!/usr/bin/env python3
# cleat: {"name":"Ports","keyword":"port","icon":"network","subtitle":"Find a TCP listener · Return requests a graceful stop","version":"2.0.1","requires":["python3","lsof"],"examples":["port 3000","port node"],"emptyMessage":"No matching TCP listener found. Try another port or process name."}
import ctypes, json, os, signal, subprocess, sys

def identity(pid):
    class Usage(ctypes.Structure):
        _fields_ = [('uuid',ctypes.c_uint8*16)] + [(name,ctypes.c_uint64) for name in ('user','system','idle','interrupt','pageins','wired','resident','footprint','start','exit')]
    usage=Usage()
    lib=ctypes.CDLL('/usr/lib/libproc.dylib')
    if lib.proc_pid_rusage(pid,0,ctypes.byref(usage)) != 0 or not usage.start:
        raise ValueError('Cannot verify process identity. Refresh Ports and retry.')
    return str(usage.start)
def main():
    if len(sys.argv)>1 and sys.argv[1]=='--run':
        request=json.loads(sys.argv[2])
        if not isinstance(request,dict) or 'pid' not in request or 'identity' not in request: raise ValueError('Invalid process identity')
        pid=request['pid']
        if not isinstance(pid,int) or pid<=1: raise ValueError('Invalid process identity')
        if identity(pid)!=request['identity']: raise ValueError('Process changed. Refresh Ports before stopping it.')
        os.kill(pid, signal.SIGKILL if request.get('force') else signal.SIGTERM)
        return
    query=(sys.argv[1] if len(sys.argv)>1 else '').casefold()
    result=subprocess.run(['lsof','-nP','-iTCP','-sTCP:LISTEN','-Fpcn'],capture_output=True,text=True)
    if result.returncode not in (0,1) or (result.returncode and result.stderr.strip()):
        raise ValueError(result.stderr.strip() or 'Could not check TCP listeners')
    pid=None; name=''; seen=set()
    for line in result.stdout.splitlines():
        if line.startswith('p'): pid=int(line[1:])
        elif line.startswith('c'): name=line[1:]
        elif line.startswith('n') and pid:
            address=line[1:]; port=address.rsplit(':',1)[-1]
            if query and not port.startswith(query) and query not in name.casefold(): continue
            if (pid,address) in seen: continue
            seen.add((pid,address))
            try: stamp=identity(pid)
            except (ValueError,OSError): continue
            arg={'pid':pid,'identity':stamp}
            print(json.dumps({'id':str(pid)+':'+address,'title':name+' · :'+port,'subtitle':'PID '+str(pid)+' · '+address+' · requests SIGTERM','arg':json.dumps(arg),'icon':'network','actionLabel':'Stop Process','actions':[{'title':'Force Stop Process','arg':json.dumps(dict(arg,force=True)),'confirmation':'Force stop '+name+' (PID '+str(pid)+')? Unsaved work may be lost.'}]}))
if __name__=='__main__':
    try: main()
    except (OSError,ValueError,subprocess.CalledProcessError) as error:
        print(str(error),file=sys.stderr); sys.exit(1)
