1+ import { readdir , readFile , readlink } from 'node:fs/promises' ;
12import { execa } from 'execa' ;
23
4+ /**
5+ * Parses a port string and returns it if valid (0-65535), otherwise undefined.
6+ */
7+ function parsePort ( value : string , radix = 10 ) : number | undefined {
8+ const port = parseInt ( value , radix ) ;
9+ if ( ! Number . isNaN ( port ) && port >= 0 && port <= 65535 ) {
10+ return port ;
11+ }
12+ return undefined ;
13+ }
14+
15+ /**
16+ * Gets listening ports for the current process on Linux by reading /proc filesystem.
17+ * This approach requires no external commands and works on all Linux systems.
18+ */
19+ async function getLinuxPort ( pid : number ) : Promise < number | undefined > {
20+ const listenState = '0A' ; // TCP LISTEN state in /proc/net/tcp
21+ const tcpFiles = [ '/proc/net/tcp' , '/proc/net/tcp6' ] as const ;
22+
23+ // Step 1: Get socket inodes from /proc/<pid>/fd/ in order
24+ // We preserve order to maintain deterministic behavior (return first port)
25+ // Use both array (for order) and Set (for O(1) lookup)
26+ const socketInodes : string [ ] = [ ] ;
27+ const socketInodesSet = new Set < string > ( ) ;
28+ const fdPath = `/proc/${ pid } /fd` ;
29+
30+ try {
31+ const fds = await readdir ( fdPath ) ;
32+ // Sort FDs numerically to ensure deterministic order (FDs are always numeric strings)
33+ const sortedFds = fds . sort ( ( a , b ) => {
34+ const numA = Number . parseInt ( a , 10 ) ;
35+ const numB = Number . parseInt ( b , 10 ) ;
36+ return numA - numB ;
37+ } ) ;
38+
39+ const results = await Promise . allSettled (
40+ sortedFds . map ( async ( fd ) => {
41+ const link = await readlink ( `${ fdPath } /${ fd } ` ) ;
42+ // Socket links look like: socket:[12345]
43+ const match = link . match ( / ^ s o c k e t : \[ ( \d + ) \] $ / ) ;
44+ return match ?. [ 1 ] ?? null ;
45+ } )
46+ ) ;
47+
48+ for ( const result of results ) {
49+ if ( result . status === 'fulfilled' && result . value ) {
50+ socketInodes . push ( result . value ) ;
51+ socketInodesSet . add ( result . value ) ;
52+ }
53+ }
54+ } catch {
55+ // Process might not exist or no permission
56+ return undefined ;
57+ }
58+
59+ if ( socketInodes . length === 0 ) {
60+ return undefined ;
61+ }
62+
63+ // Step 2: Read /proc/net/tcp and /proc/net/tcp6 to find listening sockets
64+ // Format: sl local_address rem_address st ... inode
65+ // local_address is hex IP:port, st=0A means LISTEN
66+ // We iterate through socket inodes in order to maintain deterministic behavior
67+ for ( const tcpFile of tcpFiles ) {
68+ try {
69+ const content = await readFile ( tcpFile , 'utf8' ) ;
70+ const lines = content . split ( '\n' ) . slice ( 1 ) ; // Skip header
71+
72+ // Build a map of inode -> port for quick lookup
73+ const inodeToPort = new Map < string , number > ( ) ;
74+ for ( const line of lines ) {
75+ if ( ! line . trim ( ) ) continue ; // Skip empty lines
76+
77+ const parts = line . trim ( ) . split ( / \s + / ) ;
78+ if ( parts . length < 10 ) continue ;
79+
80+ const localAddr = parts [ 1 ] ; // e.g., "00000000:0BB8" (0.0.0.0:3000)
81+ const state = parts [ 3 ] ; // "0A" = LISTEN
82+ const inode = parts [ 9 ] ;
83+
84+ if ( ! localAddr || state !== listenState || ! inode ) continue ;
85+ if ( ! socketInodesSet . has ( inode ) ) continue ;
86+
87+ // Extract port from hex format (e.g., "0BB8" -> 3000)
88+ const colonIndex = localAddr . indexOf ( ':' ) ;
89+ if ( colonIndex === - 1 ) continue ;
90+
91+ const portHex = localAddr . slice ( colonIndex + 1 ) ;
92+ if ( ! portHex ) continue ;
93+
94+ const port = parsePort ( portHex , 16 ) ;
95+ if ( port !== undefined ) {
96+ inodeToPort . set ( inode , port ) ;
97+ }
98+ }
99+
100+ // Return the first port matching our socket inodes in order
101+ for ( const inode of socketInodes ) {
102+ const port = inodeToPort . get ( inode ) ;
103+ if ( port !== undefined ) {
104+ return port ;
105+ }
106+ }
107+ } catch {
108+ // File might not exist (e.g., no IPv6 support) - continue to next file
109+ continue ;
110+ }
111+ }
112+
113+ return undefined ;
114+ }
115+
3116/**
4117 * Gets the port number that the process is listening on.
5118 * @returns The port number that the process is listening on, or undefined if the process is not listening on any port.
@@ -11,7 +124,10 @@ export async function getPort(): Promise<number | undefined> {
11124
12125 try {
13126 switch ( platform ) {
14- case 'linux' :
127+ case 'linux' : {
128+ port = await getLinuxPort ( pid ) ;
129+ break ;
130+ }
15131 case 'darwin' : {
16132 const lsofResult = await execa ( 'lsof' , [
17133 '-a' ,
@@ -28,7 +144,7 @@ export async function getPort(): Promise<number | undefined> {
28144 input : lsofResult . stdout ,
29145 }
30146 ) ;
31- port = parseInt ( awkResult . stdout . trim ( ) , 10 ) ;
147+ port = parsePort ( awkResult . stdout . trim ( ) ) ;
32148 break ;
33149 }
34150
@@ -50,8 +166,10 @@ export async function getPort(): Promise<number | undefined> {
50166 . trim ( )
51167 . match ( / ^ \s * T C P \s + (?: \[ [ \d a - f : ] + \] | [ \d . ] + ) : ( \d + ) \s + / i) ;
52168 if ( match ) {
53- port = parseInt ( match [ 1 ] , 10 ) ;
54- break ;
169+ port = parsePort ( match [ 1 ] ) ;
170+ if ( port !== undefined ) {
171+ break ;
172+ }
55173 }
56174 }
57175 }
0 commit comments