99 "errors"
1010 "fmt"
1111 "io"
12+ "net"
1213 "net/http"
1314 "net/url"
1415 "os"
@@ -24,9 +25,13 @@ import (
2425const (
2526 ServerAddr = "127.0.0.1:9901"
2627 AddressPathFlag = "--admin-address-path"
28+ runIDFlag = "--run-id"
2729 live = "live"
30+ pollInterval = 50 * time .Millisecond
2831)
2932
33+ var errMultipleEnvoyProcesses = fmt .Errorf ("multiple Envoy processes found; set %s to disambiguate" , runIDFlag )
34+
3035// NewAdminClient creates an AdminClient by polling for the admin port at
3136// adminAddressPath.
3237func NewAdminClient (ctx context.Context , client * http.Client , adminAddressPath string ) (internalapi.AdminClient , error ) {
@@ -192,7 +197,7 @@ func (c *adminClient) Get(ctx context.Context, path string) ([]byte, error) {
192197// pollAdminAddressPathForPort polls for the admin-address.txt file.
193198// It returns the admin port number or an error if the timeout is reached.
194199func pollAdminAddressPathForPort (ctx context.Context , adminAddressPath string ) (int , error ) {
195- ticker := time .NewTicker (50 * time . Millisecond )
200+ ticker := time .NewTicker (pollInterval )
196201 defer ticker .Stop ()
197202
198203 var adminAddr string
@@ -221,65 +226,166 @@ LOOP:
221226 }
222227 }
223228
224- // Parse as a URL so hostnames, IPv4, and bracketed IPv6 addresses share
225- // one port extraction path.
226- u , err := url .Parse ("http://" + adminAddr )
229+ return parseAdminPort (adminAddr )
230+ }
231+
232+ func parseAdminPort (addr string ) (int , error ) {
233+ _ , portStr , err := net .SplitHostPort (addr )
227234 if err != nil {
228235 return 0 , fmt .Errorf ("failed to parse Envoy's admin address: %w" , err )
229236 }
230237
231- port , err := strconv .Atoi (u . Port () )
238+ port , err := strconv .Atoi (portStr )
232239 if err != nil {
233240 return 0 , fmt .Errorf ("failed to parse Envoy's admin port: %w" , err )
234241 }
235-
236242 return port , nil
237243}
238244
239- // extractFlagValue parses a flag value from command line arguments.
240- func extractFlagValue (flag string , cmdline []string ) (string , error ) {
241- for i , arg := range cmdline {
242- if arg == flag && i + 1 < len (cmdline ) && cmdline [i + 1 ] != "" {
243- return cmdline [i + 1 ], nil
245+ // extractAdminAddressPath returns the first match before [internalapi.ArgsIgnoreRest].
246+ func extractAdminAddressPath (cmdline []string ) (string , error ) {
247+ for i := range len (cmdline ) {
248+ arg := cmdline [i ]
249+ if arg == internalapi .ArgsIgnoreRest {
250+ break
244251 }
245- if value , ok := strings .CutPrefix (arg , flag + "=" ); ok && value != "" {
246- return value , nil
252+ switch {
253+ case arg == AddressPathFlag && i + 1 < len (cmdline ) && cmdline [i + 1 ] != "" :
254+ return cmdline [i + 1 ], nil
255+ case strings .HasPrefix (arg , AddressPathFlag + "=" ):
256+ if value := strings .TrimPrefix (arg , AddressPathFlag + "=" ); value != "" {
257+ return value , nil
258+ }
247259 }
248260 }
249261
250262 // Shell wrappers expose the wrapped command as one argv entry. Keep this
251263 // fallback after the argv-preserving scan so direct args can contain spaces.
252264 if len (cmdline ) >= 3 && cmdline [1 ] == "-c" {
253265 fields := strings .Fields (cmdline [2 ])
254- for i , arg := range fields {
255- if arg == flag && i + 1 < len (fields ) && fields [i + 1 ] != "" {
266+ for i := range len (fields ) {
267+ arg := fields [i ]
268+ if arg == internalapi .ArgsIgnoreRest {
269+ break
270+ }
271+ switch {
272+ case arg == AddressPathFlag && i + 1 < len (fields ) && fields [i + 1 ] != "" :
256273 return fields [i + 1 ], nil
274+ case strings .HasPrefix (arg , AddressPathFlag + "=" ):
275+ if value := strings .TrimPrefix (arg , AddressPathFlag + "=" ); value != "" {
276+ return value , nil
277+ }
257278 }
258- if value , ok := strings .CutPrefix (arg , flag + "=" ); ok && value != "" {
259- return value , nil
279+ }
280+ }
281+
282+ return "" , fmt .Errorf ("%s not found in command line" , AddressPathFlag )
283+ }
284+
285+ // extractRunID returns the last match, so the func-e-appended value after [internalapi.ArgsIgnoreRest] wins.
286+ func extractRunID (cmdline []string ) (string , error ) {
287+ var runID string
288+ for i := 0 ; i < len (cmdline ); i ++ {
289+ arg := cmdline [i ]
290+ switch {
291+ case arg == runIDFlag && i + 1 < len (cmdline ) && cmdline [i + 1 ] != "" :
292+ runID = cmdline [i + 1 ]
293+ i ++
294+ case strings .HasPrefix (arg , runIDFlag + "=" ):
295+ if value := strings .TrimPrefix (arg , runIDFlag + "=" ); value != "" {
296+ runID = value
297+ }
298+ }
299+ }
300+ if runID != "" {
301+ return runID , nil
302+ }
303+
304+ if len (cmdline ) >= 3 && cmdline [1 ] == "-c" {
305+ fields := strings .Fields (cmdline [2 ])
306+ for i := 0 ; i < len (fields ); i ++ {
307+ arg := fields [i ]
308+ switch {
309+ case arg == runIDFlag && i + 1 < len (fields ) && fields [i + 1 ] != "" :
310+ runID = fields [i + 1 ]
311+ i ++
312+ case strings .HasPrefix (arg , runIDFlag + "=" ):
313+ if value := strings .TrimPrefix (arg , runIDFlag + "=" ); value != "" {
314+ runID = value
315+ }
260316 }
261317 }
262318 }
319+ if runID != "" {
320+ return runID , nil
321+ }
263322
264- return "" , fmt .Errorf ("%s not found in command line" , flag )
323+ return "" , fmt .Errorf ("%s not found in command line" , runIDFlag )
265324}
266325
267- // PollEnvoyPidAndAdminAddressPath polls for the Envoy child process and
268- // extracts its pid and admin address path from its command line.
326+ type envoyProcessCandidate struct {
327+ pid int
328+ cmdline []string
329+ }
330+
331+ func selectEnvoyProcess (candidates []envoyProcessCandidate , runID string ) (envoyPid int , adminAddressPath string , err error ) {
332+ if runID == "" {
333+ // Without an explicit runID, skip children that lack the func-e marker.
334+ foundRunID := false
335+ for _ , candidate := range candidates {
336+ if _ , err := extractRunID (candidate .cmdline ); err != nil {
337+ continue
338+ }
339+ foundRunID = true
340+
341+ path , err := extractAdminAddressPath (candidate .cmdline )
342+ if err != nil {
343+ continue
344+ }
345+ // Keep scanning past the first match to detect ambiguity.
346+ if adminAddressPath != "" {
347+ return 0 , "" , errMultipleEnvoyProcesses
348+ }
349+ envoyPid = candidate .pid
350+ adminAddressPath = path
351+ }
352+ if adminAddressPath != "" {
353+ return envoyPid , adminAddressPath , nil
354+ }
355+ if ! foundRunID {
356+ return 0 , "" , fmt .Errorf ("no child with %s" , runIDFlag )
357+ }
358+ return 0 , "" , fmt .Errorf ("no child with %s" , AddressPathFlag )
359+ }
360+
361+ for _ , candidate := range candidates {
362+ id , err := extractRunID (candidate .cmdline )
363+ if err != nil || id != runID {
364+ continue
365+ }
366+ adminAddressPath , err := extractAdminAddressPath (candidate .cmdline )
367+ if err != nil {
368+ return 0 , "" , err
369+ }
370+ return candidate .pid , adminAddressPath , nil
371+ }
372+ return 0 , "" , fmt .Errorf ("no child with %s %s" , runIDFlag , runID )
373+ }
374+
375+ // PollEnvoyPidAndAdminAddressPath polls for a child process tagged with
376+ // runID and extracts its pid and admin address path from its command line.
269377//
270- // This polls as the goroutine may be called prior to the Envoy subprocess .
271- func PollEnvoyPidAndAdminAddressPath (ctx context.Context , funcEPid int ) (envoyPid int , adminAddressPath string , err error ) {
378+ // This polls because the child may not exist yet when the caller starts .
379+ func PollEnvoyPidAndAdminAddressPath (ctx context.Context , funcEPid int , runID string ) (envoyPid int , adminAddressPath string , err error ) {
272380 funcEProc , err := process .NewProcessWithContext (ctx , int32 (funcEPid )) //nolint:gosec // funcEPid never overflows int32
273381 if err != nil {
274382 return 0 , "" , fmt .Errorf ("failed to get func-e process: %w" , err )
275383 }
276384
277- ticker := time .NewTicker (50 * time . Millisecond )
385+ ticker := time .NewTicker (pollInterval )
278386 defer ticker .Stop ()
279387
280- var envoyProc * process.Process
281388 var lastErr error
282- LOOP:
283389 for {
284390 select {
285391 case <- ctx .Done ():
@@ -299,22 +405,27 @@ LOOP:
299405 continue
300406 }
301407
302- // func-e starts one Envoy child process.
303- envoyProc = children [ 0 ]
304- envoyPid = int ( envoyProc . Pid )
305- break LOOP
306- }
307- }
308-
309- envoyCmdline , err := envoyProc . CmdlineSliceWithContext ( ctx )
310- if err != nil {
311- return 0 , "" , fmt . Errorf ( "failed to get command line of Envoy: %w" , err )
312- }
408+ candidates := make ([] envoyProcessCandidate , 0 , len ( children ))
409+ for _ , child := range children {
410+ cmdline , err := child . CmdlineSliceWithContext ( ctx )
411+ if err != nil {
412+ continue
413+ }
414+ candidates = append ( candidates , envoyProcessCandidate {
415+ pid : int ( child . Pid ),
416+ cmdline : cmdline ,
417+ } )
418+ }
313419
314- adminAddressPath , err = extractFlagValue (AddressPathFlag , envoyCmdline )
315- if err != nil {
316- return 0 , "" , err
420+ envoyPid , adminAddressPath , err = selectEnvoyProcess (candidates , runID )
421+ if err == nil {
422+ return envoyPid , adminAddressPath , nil
423+ }
424+ // Ambiguity won't resolve with more polling; the caller needs a runID.
425+ if errors .Is (err , errMultipleEnvoyProcesses ) {
426+ return 0 , "" , err
427+ }
428+ lastErr = err
429+ }
317430 }
318-
319- return envoyPid , adminAddressPath , nil
320431}
0 commit comments