tailscale/portlist/portlist_macos.go
Brad Fitzpatrick 46ce80758d portlist: update some internals to use append-style APIs
In prep for reducing garbage, being able to reuse memory.  So far this
doesn't actually reuse much. This is just changing signatures around.

But some improvement in any case:

    bradfitz@tsdev:~/src/tailscale.com$ ~/go/bin/benchstat before after
    name       old time/op    new time/op    delta
    GetList-8    11.8ms ± 9%     9.9ms ± 3%  -15.98%  (p=0.000 n=10+10)

    name       old alloc/op   new alloc/op   delta
    GetList-8    99.5kB ± 2%    91.9kB ± 0%   -7.62%  (p=0.000 n=9+9)

    name       old allocs/op  new allocs/op  delta
    GetList-8     3.05k ± 1%     2.93k ± 0%   -3.83%  (p=0.000 n=8+9)

More later, once parsers can reuse strings from previous parses.

Updates #5958

Change-Id: I76cd5048246dd24d11c4e263d8bb8041747fb2b0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-21 22:26:37 -07:00

110 lines
2.6 KiB
Go

// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build darwin && !ios
// +build darwin,!ios
package portlist
import (
"bufio"
"bytes"
"fmt"
"log"
"os/exec"
"strings"
"sync/atomic"
"time"
)
// We have to run netstat, which is a bit expensive, so don't do it too often.
const pollInterval = 5 * time.Second
func appendListeningPorts(base []Port) ([]Port, error) {
return appendListeningPortsNetstat(base, "-na")
}
var lsofFailed int64 // atomic bool
// In theory, lsof could replace the function of both listPorts() and
// addProcesses(), since it provides a superset of the netstat output.
// However, "netstat -na" runs ~100x faster than lsof on my machine, so
// we should do it only if the list of open ports has actually changed.
//
// This fails in a macOS sandbox (i.e. in the Mac App Store or System
// Extension GUI build), but does at least work in the
// tailscaled-on-macos mode.
func addProcesses(pl []Port) ([]Port, error) {
if atomic.LoadInt64(&lsofFailed) != 0 {
// This previously failed in the macOS sandbox, so don't try again.
return pl, nil
}
exe, err := exec.LookPath("lsof")
if err != nil {
return nil, fmt.Errorf("lsof: lookup: %v", err)
}
output, err := exec.Command(exe, "-F", "-n", "-P", "-O", "-S2", "-T", "-i4", "-i6").Output()
if err != nil {
var stderr []byte
if xe, ok := err.(*exec.ExitError); ok {
stderr = xe.Stderr
}
// fails when run in a macOS sandbox, so make this non-fatal.
if atomic.CompareAndSwapInt64(&lsofFailed, 0, 1) {
log.Printf("portlist: can't run lsof in Mac sandbox; omitting process names from service list. Error details: %v, %s", err, bytes.TrimSpace(stderr))
}
return pl, nil
}
type ProtoPort struct {
proto string
port uint16
}
m := map[ProtoPort]*Port{}
for i := range pl {
pp := ProtoPort{pl[i].Proto, pl[i].Port}
m[pp] = &pl[i]
}
r := bytes.NewReader(output)
scanner := bufio.NewScanner(r)
var cmd, proto string
for scanner.Scan() {
line := scanner.Text()
if line == "" {
continue
}
field, val := line[0], line[1:]
switch field {
case 'p':
// starting a new process
cmd = ""
proto = ""
case 'c':
cmd = val
case 'P':
proto = strings.ToLower(val)
case 'n':
if strings.Contains(val, "->") {
continue
}
// a listening port
port := parsePort(val)
if port > 0 {
pp := ProtoPort{proto, uint16(port)}
p := m[pp]
switch {
case p != nil:
p.Process = cmd
default:
// ignore: processes and ports come and go
}
}
}
}
return pl, nil
}