66use regex:: Regex ;
77use std:: fs:: read_link;
88use std:: hash:: Hash ;
9+ #[ cfg( target_os = "linux" ) ]
10+ use std:: ops:: RangeInclusive ;
911use std:: sync:: { LazyLock , OnceLock } ;
1012use std:: {
1113 collections:: HashMap ,
@@ -15,20 +17,130 @@ use std::{
1517} ;
1618use walkdir:: { DirEntry , WalkDir } ;
1719
20+ /// Represents a TTY driver entry from /proc/tty/drivers
21+ #[ cfg( target_os = "linux" ) ]
22+ #[ derive( Debug , Clone , PartialEq , Eq ) ]
23+ struct TtyDriverEntry {
24+ device_prefix : String ,
25+ major : u32 ,
26+ minor_range : RangeInclusive < u32 > ,
27+ }
28+
29+ #[ cfg( target_os = "linux" ) ]
30+ impl TtyDriverEntry {
31+ fn new ( device_prefix : String , major : u32 , minor_range : RangeInclusive < u32 > ) -> Self {
32+ Self {
33+ device_prefix,
34+ major,
35+ minor_range,
36+ }
37+ }
38+
39+ fn device_path_if_matches ( & self , major : u32 , minor : u32 ) -> Option < String > {
40+ if self . major != major || !self . minor_range . contains ( & minor) {
41+ return None ;
42+ }
43+
44+ // /dev/pts devices are in a subdirectory unlike others
45+ if self . device_prefix == "/dev/pts" {
46+ return Some ( format ! ( "/dev/pts/{}" , minor) ) ;
47+ }
48+
49+ // If there is only one minor (e.g. /dev/console) it should not get a number
50+ if self . minor_range . start ( ) == self . minor_range . end ( ) {
51+ Some ( self . device_prefix . clone ( ) )
52+ } else {
53+ let device_number = minor - self . minor_range . start ( ) ;
54+ Some ( format ! ( "{}{}" , self . device_prefix, device_number) )
55+ }
56+ }
57+ }
58+
59+ #[ cfg( target_os = "linux" ) ]
60+ static TTY_DRIVERS_CACHE : LazyLock < Vec < TtyDriverEntry > > = LazyLock :: new ( || {
61+ fs:: read_to_string ( "/proc/tty/drivers" )
62+ . map ( |content| parse_proc_tty_drivers ( & content) )
63+ . unwrap_or_default ( )
64+ } ) ;
65+
66+ #[ cfg( target_os = "linux" ) ]
67+ fn parse_proc_tty_drivers ( drivers_content : & str ) -> Vec < TtyDriverEntry > {
68+ // Example lines:
69+ // /dev/tty /dev/tty 5 0 system:/dev/tty
70+ // /dev/vc/0 /dev/vc/0 4 0 system:vtmaster
71+ // hvc /dev/hvc 229 0-7 system
72+ // serial /dev/ttyS 4 64-95 serial
73+ // pty_slave /dev/pts 136 0-1048575 pty:slave
74+ let regex = Regex :: new ( r"^[^ ]+ +([^ ]+) +(\d+) +(\d+)(?:-(\d+))?" ) . unwrap ( ) ;
75+
76+ let mut entries = Vec :: new ( ) ;
77+ for line in drivers_content. lines ( ) {
78+ let Some ( captures) = regex. captures ( line) else {
79+ continue ;
80+ } ;
81+
82+ let device_prefix = captures[ 1 ] . to_string ( ) ;
83+ let Ok ( major) = captures[ 2 ] . parse :: < u32 > ( ) else {
84+ continue ;
85+ } ;
86+ let Ok ( min_minor) = captures[ 3 ] . parse :: < u32 > ( ) else {
87+ continue ;
88+ } ;
89+ let max_minor = captures
90+ . get ( 4 )
91+ . and_then ( |m| m. as_str ( ) . parse :: < u32 > ( ) . ok ( ) )
92+ . unwrap_or ( min_minor) ;
93+
94+ entries. push ( TtyDriverEntry :: new (
95+ device_prefix,
96+ major,
97+ min_minor..=max_minor,
98+ ) ) ;
99+ }
100+
101+ entries
102+ }
103+
18104#[ derive( Debug , Clone , PartialEq , Eq , Hash ) ]
19105pub enum Teletype {
20- Tty ( u64 ) ,
21- TtyS ( u64 ) ,
22- Pts ( u64 ) ,
106+ Known ( String ) ,
23107 Unknown ,
24108}
25109
110+ impl Teletype {
111+ #[ cfg( target_os = "linux" ) ]
112+ pub fn from_tty_nr ( tty_nr : u64 ) -> Self {
113+ Self :: from_tty_nr_impl ( tty_nr, & TTY_DRIVERS_CACHE )
114+ }
115+
116+ #[ cfg( not( target_os = "linux" ) ) ]
117+ pub fn from_tty_nr ( _tty_nr : u64 ) -> Self {
118+ Self :: Unknown
119+ }
120+
121+ #[ cfg( target_os = "linux" ) ]
122+ fn from_tty_nr_impl ( tty_nr : u64 , drivers : & [ TtyDriverEntry ] ) -> Self {
123+ use uucore:: libc:: { major, minor} ;
124+
125+ if tty_nr == 0 {
126+ return Self :: Unknown ;
127+ }
128+
129+ let ( major_dev, minor_dev) = ( major ( tty_nr) , minor ( tty_nr) ) ;
130+ for entry in drivers. iter ( ) {
131+ if let Some ( device_path) = entry. device_path_if_matches ( major_dev, minor_dev) {
132+ return Self :: Known ( device_path) ;
133+ }
134+ }
135+
136+ Self :: Unknown
137+ }
138+ }
139+
26140impl Display for Teletype {
27141 fn fmt ( & self , f : & mut Formatter ) -> fmt:: Result {
28142 match self {
29- Self :: Tty ( id) => write ! ( f, "/dev/pts/{id}" ) ,
30- Self :: TtyS ( id) => write ! ( f, "/dev/tty{id}" ) ,
31- Self :: Pts ( id) => write ! ( f, "/dev/ttyS{id}" ) ,
143+ Self :: Known ( device_path) => write ! ( f, "{}" , device_path) ,
32144 Self :: Unknown => write ! ( f, "?" ) ,
33145 }
34146 }
@@ -58,43 +170,8 @@ impl TryFrom<PathBuf> for Teletype {
58170 type Error = ( ) ;
59171
60172 fn try_from ( value : PathBuf ) -> Result < Self , Self :: Error > {
61- // Three case: /dev/pts/* , /dev/ttyS**, /dev/tty**
62-
63- let mut iter = value. iter ( ) ;
64- // Case 1
65-
66- // Considering this format: **/**/pts/<num>
67- if let ( Some ( _) , Some ( num) ) = ( iter. find ( |it| * it == "pts" ) , iter. next ( ) ) {
68- return num
69- . to_str ( )
70- . ok_or ( ( ) ) ?
71- . parse :: < u64 > ( )
72- . map_err ( |_| ( ) )
73- . map ( Teletype :: Pts ) ;
74- } ;
75-
76- // Considering this format: **/**/ttyS** then **/**/tty**
77- let path = value. to_str ( ) . ok_or ( ( ) ) ?;
78-
79- let f = |prefix : & str | {
80- value
81- . iter ( )
82- . next_back ( ) ?
83- . to_str ( ) ?
84- . strip_prefix ( prefix) ?
85- . parse :: < u64 > ( )
86- . ok ( )
87- } ;
88-
89- if path. contains ( "ttyS" ) {
90- // Case 2
91- f ( "ttyS" ) . ok_or ( ( ) ) . map ( Teletype :: TtyS )
92- } else if path. contains ( "tty" ) {
93- // Case 3
94- f ( "tty" ) . ok_or ( ( ) ) . map ( Teletype :: Tty )
95- } else {
96- Err ( ( ) )
97- }
173+ let path_str = value. to_str ( ) . ok_or ( ( ) ) ?;
174+ Ok ( Self :: Known ( path_str. to_string ( ) ) )
98175 }
99176}
100177
@@ -602,28 +679,17 @@ impl ProcessInformation {
602679 RunState :: try_from ( self . stat ( ) . get ( 2 ) . unwrap ( ) . as_str ( ) )
603680 }
604681
605- /// This function will scan the `/proc/<pid>/fd` directory
606- ///
607- /// If the process does not belong to any terminal and mismatched permission,
608- /// the result will contain [TerminalType::Unknown].
682+ /// Get the controlling terminal from the tty_nr field in /proc/<pid>/stat
609683 ///
610- /// Otherwise [TerminalType ::Unknown] does not appear in the result.
611- pub fn tty ( & self ) -> Teletype {
612- let path = PathBuf :: from ( format ! ( "/proc/{}/fd" , self . pid ) ) ;
613-
614- let Ok ( result ) = fs :: read_dir ( path ) else {
615- return Teletype :: Unknown ;
684+ /// Returns Teletype ::Unknown if the process has no controlling terminal (tty_nr == 0)
685+ /// or if the tty_nr cannot be resolved to a device.
686+ pub fn tty ( & mut self ) -> Teletype {
687+ let tty_nr = match self . get_numeric_stat_field ( 6 ) {
688+ Ok ( tty_nr ) => tty_nr ,
689+ Err ( _ ) => return Teletype :: Unknown ,
616690 } ;
617691
618- for dir in result. flatten ( ) . filter ( |it| it. path ( ) . is_symlink ( ) ) {
619- if let Ok ( path) = fs:: read_link ( dir. path ( ) ) {
620- if let Ok ( tty) = Teletype :: try_from ( path) {
621- return tty;
622- }
623- }
624- }
625-
626- Teletype :: Unknown
692+ Teletype :: from_tty_nr ( tty_nr)
627693 }
628694
629695 pub fn thread_ids ( & mut self ) -> & [ usize ] {
@@ -734,6 +800,53 @@ mod tests {
734800 #[ cfg( target_os = "linux" ) ]
735801 use uucore:: process:: getpid;
736802
803+ #[ test]
804+ #[ cfg( target_os = "linux" ) ]
805+ fn test_tty_resolution ( ) {
806+ let test_content = r#"/dev/tty /dev/tty 5 0 system:/dev/tty
807+ /dev/console /dev/console 5 1 system:console
808+ /dev/ptmx /dev/ptmx 5 2 system
809+ /dev/vc/0 /dev/vc/0 4 0 system:vtmaster
810+ hvc /dev/hvc 229 0-7 system
811+ serial /dev/ttyS 4 64-95 serial
812+ pty_slave /dev/pts 136 0-1048575 pty:slave
813+ pty_master /dev/ptm 128 0-1048575 pty:master
814+ unknown /dev/tty 4 1-63 console"# ;
815+
816+ let expected_entries = vec ! [
817+ TtyDriverEntry :: new( "/dev/tty" . to_string( ) , 5 , 0 ..=0 ) ,
818+ TtyDriverEntry :: new( "/dev/console" . to_string( ) , 5 , 1 ..=1 ) ,
819+ TtyDriverEntry :: new( "/dev/ptmx" . to_string( ) , 5 , 2 ..=2 ) ,
820+ TtyDriverEntry :: new( "/dev/vc/0" . to_string( ) , 4 , 0 ..=0 ) ,
821+ TtyDriverEntry :: new( "/dev/hvc" . to_string( ) , 229 , 0 ..=7 ) ,
822+ TtyDriverEntry :: new( "/dev/ttyS" . to_string( ) , 4 , 64 ..=95 ) ,
823+ TtyDriverEntry :: new( "/dev/pts" . to_string( ) , 136 , 0 ..=1048575 ) ,
824+ TtyDriverEntry :: new( "/dev/ptm" . to_string( ) , 128 , 0 ..=1048575 ) ,
825+ TtyDriverEntry :: new( "/dev/tty" . to_string( ) , 4 , 1 ..=63 ) ,
826+ ] ;
827+
828+ let parsed_entries = parse_proc_tty_drivers ( test_content) ;
829+ assert_eq ! ( parsed_entries, expected_entries) ;
830+
831+ let test_cases = vec ! [
832+ // (major, minor, expected_result)
833+ ( 0 , 0 , Teletype :: Unknown ) ,
834+ ( 5 , 0 , Teletype :: Known ( "/dev/tty" . to_string( ) ) ) ,
835+ ( 5 , 1 , Teletype :: Known ( "/dev/console" . to_string( ) ) ) ,
836+ ( 136 , 123 , Teletype :: Known ( "/dev/pts/123" . to_string( ) ) ) ,
837+ ( 4 , 64 , Teletype :: Known ( "/dev/ttyS0" . to_string( ) ) ) ,
838+ ( 4 , 65 , Teletype :: Known ( "/dev/ttyS1" . to_string( ) ) ) ,
839+ ( 229 , 3 , Teletype :: Known ( "/dev/hvc3" . to_string( ) ) ) ,
840+ ( 999 , 999 , Teletype :: Unknown ) ,
841+ ] ;
842+
843+ for ( major, minor, expected) in test_cases {
844+ let tty_nr = uucore:: libc:: makedev ( major, minor) ;
845+ let result = Teletype :: from_tty_nr_impl ( tty_nr, & parsed_entries) ;
846+ assert_eq ! ( result, expected) ;
847+ }
848+ }
849+
737850 #[ test]
738851 fn test_run_state_conversion ( ) {
739852 assert_eq ! ( RunState :: try_from( "R" ) . unwrap( ) , RunState :: Running ) ;
@@ -763,7 +876,14 @@ mod tests {
763876 #[ test]
764877 #[ cfg( target_os = "linux" ) ]
765878 fn test_pid_entry ( ) {
766- let pid_entry = ProcessInformation :: current_process_info ( ) . unwrap ( ) ;
879+ use std:: io:: IsTerminal ;
880+
881+ let mut pid_entry = ProcessInformation :: current_process_info ( ) . unwrap ( ) ;
882+
883+ if !std:: io:: stdout ( ) . is_terminal ( ) && !std:: io:: stderr ( ) . is_terminal ( ) {
884+ assert_eq ! ( pid_entry. tty( ) , Teletype :: Unknown ) ;
885+ return ;
886+ }
767887 let mut result = WalkDir :: new ( format ! ( "/proc/{}/fd" , getpid( ) ) )
768888 . into_iter ( )
769889 . flatten ( )
0 commit comments