@@ -669,6 +669,94 @@ impl PtyManager {
669669 }
670670 }
671671
672+ /// Batch version of `get_service_pids` for multiple terminals at once.
673+ /// On Linux with dtach, reads `/proc` once instead of spawning `lsof` per terminal.
674+ pub fn get_batch_service_pids ( & self , terminal_ids : & [ & str ] ) -> HashMap < String , Vec < u32 > > {
675+ #[ cfg( unix) ]
676+ {
677+ match self . session_backend {
678+ ResolvedBackend :: Dtach => {
679+ return self . get_batch_dtach_service_pids ( terminal_ids) ;
680+ }
681+ _ => { }
682+ }
683+ }
684+ // Fallback: call per-terminal method
685+ terminal_ids
686+ . iter ( )
687+ . map ( |tid| ( tid. to_string ( ) , self . get_service_pids ( tid) ) )
688+ . collect ( )
689+ }
690+
691+ /// Batch dtach PID lookup. On Linux, reads `/proc/net/unix` + `/proc/*/fd/`
692+ /// once for all sockets. On other Unix, falls back to lsof per terminal.
693+ #[ cfg( unix) ]
694+ fn get_batch_dtach_service_pids ( & self , terminal_ids : & [ & str ] ) -> HashMap < String , Vec < u32 > > {
695+ // Collect socket paths for all terminals
696+ let mut socket_to_terminal: HashMap < std:: path:: PathBuf , & str > = HashMap :: new ( ) ;
697+ let mut attach_pids: HashMap < & str , Option < u32 > > = HashMap :: new ( ) ;
698+
699+ for & tid in terminal_ids {
700+ let session_name = self . session_backend . session_name ( tid) ;
701+ if let Some ( p) = self . session_backend . socket_path ( & session_name) {
702+ if p. exists ( ) {
703+ socket_to_terminal. insert ( p, tid) ;
704+ attach_pids. insert ( tid, self . get_shell_pid ( tid) ) ;
705+ }
706+ }
707+ }
708+
709+ // Resolve PIDs for all sockets at once
710+ let socket_pids = find_pids_for_unix_sockets (
711+ & socket_to_terminal. keys ( ) . cloned ( ) . collect :: < Vec < _ > > ( ) ,
712+ ) ;
713+
714+ // Build result map
715+ let mut result: HashMap < String , Vec < u32 > > = HashMap :: new ( ) ;
716+ for ( & tid, _) in & attach_pids {
717+ let session_name = self . session_backend . session_name ( tid) ;
718+ let socket_path = match self . session_backend . socket_path ( & session_name) {
719+ Some ( p) => p,
720+ None => {
721+ result. insert (
722+ tid. to_string ( ) ,
723+ self . get_shell_pid ( tid) . into_iter ( ) . collect ( ) ,
724+ ) ;
725+ continue ;
726+ }
727+ } ;
728+
729+ let attach_pid = attach_pids. get ( tid) . copied ( ) . flatten ( ) ;
730+ let pids: Vec < u32 > = socket_pids
731+ . get ( & socket_path)
732+ . map ( |pids| {
733+ pids. iter ( )
734+ . copied ( )
735+ . filter ( |pid| Some ( * pid) != attach_pid)
736+ . collect ( )
737+ } )
738+ . unwrap_or_default ( ) ;
739+
740+ if pids. is_empty ( ) {
741+ result. insert (
742+ tid. to_string ( ) ,
743+ self . get_shell_pid ( tid) . into_iter ( ) . collect ( ) ,
744+ ) ;
745+ } else {
746+ result. insert ( tid. to_string ( ) , pids) ;
747+ }
748+ }
749+
750+ // Terminals without a valid socket path
751+ for & tid in terminal_ids {
752+ result
753+ . entry ( tid. to_string ( ) )
754+ . or_insert_with ( || self . get_shell_pid ( tid) . into_iter ( ) . collect ( ) ) ;
755+ }
756+
757+ result
758+ }
759+
672760 /// Check if the session backend handles mouse events (tmux with mouse on)
673761 pub fn uses_mouse_backend ( & self ) -> bool {
674762 matches ! ( self . session_backend, ResolvedBackend :: Tmux )
@@ -847,3 +935,179 @@ fn wait_for_exit_code(pid: u32) -> Option<u32> {
847935 }
848936}
849937
938+ /// Find which PIDs have the given Unix sockets open.
939+ ///
940+ /// On Linux, reads `/proc/net/unix` to map socket paths to inode numbers,
941+ /// then scans `/proc/*/fd/` to find PIDs holding those inodes.
942+ /// On other Unix systems, falls back to a single `lsof` invocation.
943+ #[ cfg( unix) ]
944+ fn find_pids_for_unix_sockets (
945+ socket_paths : & [ std:: path:: PathBuf ] ,
946+ ) -> HashMap < std:: path:: PathBuf , Vec < u32 > > {
947+ if socket_paths. is_empty ( ) {
948+ return HashMap :: new ( ) ;
949+ }
950+
951+ #[ cfg( target_os = "linux" ) ]
952+ {
953+ find_pids_for_unix_sockets_linux ( socket_paths)
954+ }
955+
956+ #[ cfg( not( target_os = "linux" ) ) ]
957+ {
958+ find_pids_for_unix_sockets_lsof ( socket_paths)
959+ }
960+ }
961+
962+ /// Linux implementation: read `/proc/net/unix` and `/proc/*/fd/` — no subprocess spawning.
963+ #[ cfg( target_os = "linux" ) ]
964+ fn find_pids_for_unix_sockets_linux (
965+ socket_paths : & [ std:: path:: PathBuf ] ,
966+ ) -> HashMap < std:: path:: PathBuf , Vec < u32 > > {
967+ // Step 1: Read /proc/net/unix to find inodes for our socket paths.
968+ // Format: "Num RefCount Protocol Flags Type St Inode Path"
969+ let proc_net = match std:: fs:: read_to_string ( "/proc/net/unix" ) {
970+ Ok ( s) => s,
971+ Err ( _) => return HashMap :: new ( ) ,
972+ } ;
973+
974+ // Build a set of canonical socket paths for fast lookup
975+ let canonical_paths: HashMap < std:: path:: PathBuf , & std:: path:: PathBuf > = socket_paths
976+ . iter ( )
977+ . filter_map ( |p| std:: fs:: canonicalize ( p) . ok ( ) . map ( |c| ( c, p) ) )
978+ . collect ( ) ;
979+
980+ // Map inode -> original socket path
981+ let mut inode_to_path: HashMap < u64 , & std:: path:: PathBuf > = HashMap :: new ( ) ;
982+ for line in proc_net. lines ( ) . skip ( 1 ) {
983+ // Fields are space-separated; path is the last field (may be absent)
984+ let fields: Vec < & str > = line. split_whitespace ( ) . collect ( ) ;
985+ if fields. len ( ) < 8 {
986+ continue ;
987+ }
988+ let inode: u64 = match fields[ 6 ] . parse ( ) {
989+ Ok ( i) => i,
990+ Err ( _) => continue ,
991+ } ;
992+ let path_str = fields[ 7 ] ;
993+ let path = std:: path:: Path :: new ( path_str) ;
994+
995+ // Check against canonical paths
996+ if let Some ( & orig) = canonical_paths. get ( path) {
997+ inode_to_path. insert ( inode, orig) ;
998+ } else if let Ok ( canon) = std:: fs:: canonicalize ( path) {
999+ if let Some ( & orig) = canonical_paths. get ( & canon) {
1000+ inode_to_path. insert ( inode, orig) ;
1001+ }
1002+ }
1003+ }
1004+
1005+ if inode_to_path. is_empty ( ) {
1006+ return HashMap :: new ( ) ;
1007+ }
1008+
1009+ // Step 2: Scan /proc/*/fd/ to find PIDs that hold these inodes.
1010+ let mut result: HashMap < std:: path:: PathBuf , Vec < u32 > > = HashMap :: new ( ) ;
1011+
1012+ let proc_dir = match std:: fs:: read_dir ( "/proc" ) {
1013+ Ok ( d) => d,
1014+ Err ( _) => return HashMap :: new ( ) ,
1015+ } ;
1016+
1017+ for entry in proc_dir. flatten ( ) {
1018+ let pid: u32 = match entry. file_name ( ) . to_str ( ) . and_then ( |s| s. parse ( ) . ok ( ) ) {
1019+ Some ( p) => p,
1020+ None => continue ,
1021+ } ;
1022+
1023+ let fd_dir = entry. path ( ) . join ( "fd" ) ;
1024+ let fd_entries = match std:: fs:: read_dir ( & fd_dir) {
1025+ Ok ( d) => d,
1026+ Err ( _) => continue , // permission denied or process gone
1027+ } ;
1028+
1029+ for fd_entry in fd_entries. flatten ( ) {
1030+ // readlink on /proc/<pid>/fd/<n> gives "socket:[<inode>]"
1031+ let link = match std:: fs:: read_link ( fd_entry. path ( ) ) {
1032+ Ok ( l) => l,
1033+ Err ( _) => continue ,
1034+ } ;
1035+ let link_str = match link. to_str ( ) {
1036+ Some ( s) => s,
1037+ None => continue ,
1038+ } ;
1039+ // Parse "socket:[12345]"
1040+ if let Some ( inode_str) = link_str
1041+ . strip_prefix ( "socket:[" )
1042+ . and_then ( |s| s. strip_suffix ( ']' ) )
1043+ {
1044+ if let Ok ( inode) = inode_str. parse :: < u64 > ( ) {
1045+ if let Some ( & socket_path) = inode_to_path. get ( & inode) {
1046+ result
1047+ . entry ( socket_path. clone ( ) )
1048+ . or_default ( )
1049+ . push ( pid) ;
1050+ }
1051+ // Early exit if we found all inodes
1052+ // (not worth the bookkeeping for a small set)
1053+ }
1054+ }
1055+ }
1056+ }
1057+
1058+ result
1059+ }
1060+
1061+ /// Fallback for non-Linux Unix: single `lsof` call for all sockets.
1062+ #[ cfg( all( unix, not( target_os = "linux" ) ) ) ]
1063+ fn find_pids_for_unix_sockets_lsof (
1064+ socket_paths : & [ std:: path:: PathBuf ] ,
1065+ ) -> HashMap < std:: path:: PathBuf , Vec < u32 > > {
1066+ // lsof can take multiple file arguments at once
1067+ let mut cmd = crate :: process:: command ( "lsof" ) ;
1068+ cmd. arg ( "-t" ) ;
1069+ for path in socket_paths {
1070+ cmd. arg ( path) ;
1071+ }
1072+
1073+ let output = match crate :: process:: safe_output ( & mut cmd) {
1074+ Ok ( o) if o. status . success ( ) => o,
1075+ _ => return HashMap :: new ( ) ,
1076+ } ;
1077+
1078+ // lsof -t with multiple files just lists PIDs (no file association).
1079+ // We need per-file results, so use full output instead.
1080+ drop ( output) ;
1081+
1082+ let mut cmd = crate :: process:: command ( "lsof" ) ;
1083+ cmd. arg ( "-F" ) . arg ( "pn" ) ; // machine-readable: p=PID, n=name fields
1084+ for path in socket_paths {
1085+ cmd. arg ( path) ;
1086+ }
1087+
1088+ let output = match crate :: process:: safe_output ( & mut cmd) {
1089+ Ok ( o) if o. status . success ( ) => o,
1090+ _ => return HashMap :: new ( ) ,
1091+ } ;
1092+
1093+ let stdout = String :: from_utf8_lossy ( & output. stdout ) ;
1094+ let mut result: HashMap < std:: path:: PathBuf , Vec < u32 > > = HashMap :: new ( ) ;
1095+ let mut current_pid: Option < u32 > = None ;
1096+
1097+ // lsof -F output: lines starting with 'p' = PID, 'n' = name (path)
1098+ for line in stdout. lines ( ) {
1099+ if let Some ( pid_str) = line. strip_prefix ( 'p' ) {
1100+ current_pid = pid_str. parse ( ) . ok ( ) ;
1101+ } else if let Some ( name) = line. strip_prefix ( 'n' ) {
1102+ if let Some ( pid) = current_pid {
1103+ let path = std:: path:: PathBuf :: from ( name) ;
1104+ if socket_paths. contains ( & path) {
1105+ result. entry ( path) . or_default ( ) . push ( pid) ;
1106+ }
1107+ }
1108+ }
1109+ }
1110+
1111+ result
1112+ }
1113+
0 commit comments