From 21ef7e5c35bb4f792884a76601111bf871215532 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Fri, 4 Nov 2022 06:41:36 -0700 Subject: [PATCH] portlist: add macOS osImpl, finish migration to new style Previously: * 036f70b7b4 for linux * 35bee36549 for windows This does macOS. And removes all the compat code for the old style. (e.g. iOS, js are no longer mentioned; all platforms without implementations just default to not doing anything) One possible regression is that platforms without explicit implementations previously tried to do the "netstat -na" style to get open ports (but not process names). Maybe that worked on FreeBSD and OpenBSD previously, but nobody ever really tested it. And it was kinda useless without associated process names. So better off removing those for now until they get a good implementation. Signed-off-by: Brad Fitzpatrick --- portlist/netstat.go | 118 ++++++++++----------- portlist/netstat_exec.go | 31 ------ portlist/netstat_test.go | 55 ++++------ portlist/poller.go | 43 +++----- portlist/portlist_ios.go | 22 ---- portlist/portlist_js.go | 17 ---- portlist/portlist_linux.go | 13 +-- portlist/portlist_macos.go | 192 +++++++++++++++++++++++++++-------- portlist/portlist_other.go | 22 ---- portlist/portlist_windows.go | 16 +-- 10 files changed, 239 insertions(+), 290 deletions(-) delete mode 100644 portlist/netstat_exec.go delete mode 100644 portlist/portlist_ios.go delete mode 100644 portlist/portlist_js.go delete mode 100644 portlist/portlist_other.go diff --git a/portlist/netstat.go b/portlist/netstat.go index ebfd5fb60..e0a25cbe3 100644 --- a/portlist/netstat.go +++ b/portlist/netstat.go @@ -2,21 +2,30 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !ios && !js +//go:build darwin && !ios package portlist import ( - "sort" - "strconv" - "strings" + "bufio" + "bytes" + "io" + + "go4.org/mem" ) -func parsePort(s string) int { +// parsePort returns the port number at the end of s following the last "." or +// ":", whichever comes last. It returns -1 on a parse error or invalid number +// and 0 if the port number was "*". +// +// This is basically net.SplitHostPort except that it handles a "." (as macOS +// and others return in netstat output), uses mem.RO, and validates that the +// port must be numeric and in the uint16 range. +func parsePort(s mem.RO) int { // a.b.c.d:1234 or [a:b:c:d]:1234 - i1 := strings.LastIndexByte(s, ':') + i1 := mem.LastIndexByte(s, ':') // a.b.c.d.1234 or [a:b:c:d].1234 - i2 := strings.LastIndexByte(s, '.') + i2 := mem.LastIndexByte(s, '.') i := i1 if i2 > i { @@ -27,12 +36,12 @@ func parsePort(s string) int { return -1 } - portstr := s[i+1:] - if portstr == "*" { + portstr := s.SliceFrom(i + 1) + if portstr.EqualString("*") { return 0 } - port, err := strconv.ParseUint(portstr, 10, 16) + port, err := mem.ParseUint(portstr, 10, 16) if err != nil { // invalid port; weird return -1 @@ -41,34 +50,45 @@ func parsePort(s string) int { return int(port) } -func isLoopbackAddr(s string) bool { - return strings.HasPrefix(s, "127.") || - strings.HasPrefix(s, "[::1]:") || - strings.HasPrefix(s, "::1.") +func isLoopbackAddr(s mem.RO) bool { + return mem.HasPrefix(s, mem.S("127.")) || + mem.HasPrefix(s, mem.S("[::1]:")) || + mem.HasPrefix(s, mem.S("::1.")) } type nothing struct{} -// Lowest common denominator parser for "netstat -na" format. +// appendParsePortsNetstat appends to base listening ports +// from "netstat" output, read from br. See TestParsePortsNetstat +// for example input lines. +// +// This used to be a lowest common denominator parser for "netstat -na" format. // All of Linux, Windows, and macOS support -na and give similar-ish output // formats that we can parse without special detection logic. // Unfortunately, options to filter by proto or state are non-portable, // so we'll filter for ourselves. -func appendParsePortsNetstat(base []Port, output string) []Port { - m := map[Port]nothing{} - lines := strings.Split(string(output), "\n") - - var lastline string - var lastport Port - for _, line := range lines { - trimline := strings.TrimSpace(line) - cols := strings.Fields(trimline) +// Nowadays, though, we only use it for macOS as of 2022-11-04. +func appendParsePortsNetstat(base []Port, br *bufio.Reader) ([]Port, error) { + ret := base + var fieldBuf [10]mem.RO + for { + line, err := br.ReadBytes('\n') + if err != nil { + if err == io.EOF { + break + } + return nil, err + } + trimline := bytes.TrimSpace(line) + cols := mem.AppendFields(fieldBuf[:0], mem.B(trimline)) if len(cols) < 1 { continue } - protos := strings.ToLower(cols[0]) - var proto, laddr, raddr string - if strings.HasPrefix(protos, "tcp") { + protos := cols[0] + + var proto string + var laddr, raddr mem.RO + if mem.HasPrefixFold(protos, mem.S("tcp")) { if len(cols) < 4 { continue } @@ -76,7 +96,7 @@ func appendParsePortsNetstat(base []Port, output string) []Port { laddr = cols[len(cols)-3] raddr = cols[len(cols)-2] state := cols[len(cols)-1] - if !strings.HasPrefix(state, "LISTEN") { + if !mem.HasPrefix(state, mem.S("LISTEN")) { // not interested in non-listener sockets continue } @@ -84,7 +104,7 @@ func appendParsePortsNetstat(base []Port, output string) []Port { // not interested in loopback-bound listeners continue } - } else if strings.HasPrefix(protos, "udp") { + } else if mem.HasPrefixFold(protos, mem.S("udp")) { if len(cols) < 3 { continue } @@ -95,53 +115,21 @@ func appendParsePortsNetstat(base []Port, output string) []Port { // not interested in loopback-bound listeners continue } - } else if protos[0] == '[' && len(trimline) > 2 { - // Windows: with netstat -nab, appends a line like: - // [description] - // after the port line. - p := lastport - delete(m, lastport) - proc := trimline[1 : len(trimline)-1] - if proc == "svchost.exe" && lastline != "" { - p.Process = argvSubject(lastline) - } else { - p.Process = argvSubject(proc) - } - m[p] = nothing{} } else { // not interested in other protocols - lastline = trimline continue } lport := parsePort(laddr) rport := parsePort(raddr) - if rport != 0 || lport <= 0 { + if rport > 0 || lport <= 0 { // not interested in "connected" sockets continue } - - p := Port{ + ret = append(ret, Port{ Proto: proto, Port: uint16(lport), - } - m[p] = nothing{} - lastport = p - lastline = "" + }) } - - ret := base - for p := range m { - ret = append(ret, p) - } - - // Only sort the part we appended. It's up to the caller to sort the whole - // thing if they'd like. In practice the caller's base will have len 0, - // though, so the whole thing will be sorted. - toSort := ret[len(base):] - sort.Slice(toSort, func(i, j int) bool { - return (&toSort[i]).lessThan(&toSort[j]) - }) - - return ret + return ret, nil } diff --git a/portlist/netstat_exec.go b/portlist/netstat_exec.go deleted file mode 100644 index 25e23b8f6..000000000 --- a/portlist/netstat_exec.go +++ /dev/null @@ -1,31 +0,0 @@ -// 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 (windows || freebsd || openbsd || darwin) && !ios && !js - -package portlist - -import ( - "fmt" - "os/exec" - "strings" -) - -func appendListeningPortsNetstat(base []Port, arg string) ([]Port, error) { - exe, err := exec.LookPath("netstat") - if err != nil { - return nil, fmt.Errorf("netstat: lookup: %v", err) - } - output, err := exec.Command(exe, arg).Output() - if err != nil { - xe, ok := err.(*exec.ExitError) - stderr := "" - if ok { - stderr = strings.TrimSpace(string(xe.Stderr)) - } - return nil, fmt.Errorf("netstat: %v (%q)", err, stderr) - } - - return appendParsePortsNetstat(base, string(output)), nil -} diff --git a/portlist/netstat_test.go b/portlist/netstat_test.go index 1887e2c95..29a74b1c6 100644 --- a/portlist/netstat_test.go +++ b/portlist/netstat_test.go @@ -2,13 +2,17 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !ios && !js +//go:build darwin && !ios package portlist import ( + "bufio" "encoding/json" + "strings" "testing" + + "go4.org/mem" ) func TestParsePort(t *testing.T) { @@ -27,7 +31,7 @@ func TestParsePort(t *testing.T) { } for _, io := range tests { - got := parsePort(io.in) + got := parsePort(mem.S(io.in)) if got != io.expect { t.Fatalf("input:%#v expect:%v got:%v\n", io.in, io.expect, got) } @@ -35,12 +39,6 @@ func TestParsePort(t *testing.T) { } const netstatOutput = ` -// linux -tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -udp 0 0 0.0.0.0:5353 0.0.0.0:* -udp6 0 0 :::5353 :::* -udp6 0 0 :::5354 :::* - // macOS tcp4 0 0 *.23 *.* LISTEN tcp6 0 0 *.24 *.* LISTEN @@ -49,43 +47,26 @@ tcp4 0 0 127.0.0.1.8186 *.* LISTEN tcp6 0 0 ::1.8187 *.* LISTEN tcp4 0 0 127.1.2.3.8188 *.* LISTEN -udp6 0 0 *.5453 *.* -udp4 0 0 *.5553 *.* - -// Windows 10 - Proto Local Address Foreign Address State - TCP 0.0.0.0:32 0.0.0.0:0 LISTENING - [sshd.exe] - UDP 0.0.0.0:5050 *:* - CDPSvc - [svchost.exe] - UDP 0.0.0.0:53 *:* - [chrome.exe] - UDP 10.0.1.43:9353 *:* - [iTunes.exe] - UDP [::]:53 *:* - UDP [::]:53 *:* - [funball.exe] +udp6 0 0 *.106 *.* +udp4 0 0 *.104 *.* +udp46 0 0 *.146 *.* ` func TestParsePortsNetstat(t *testing.T) { want := List{ - Port{"tcp", 22, ""}, Port{"tcp", 23, ""}, Port{"tcp", 24, ""}, - Port{"tcp", 32, "sshd"}, - Port{"udp", 53, "chrome"}, - Port{"udp", 53, "funball"}, - Port{"udp", 5050, "CDPSvc"}, - Port{"udp", 5353, ""}, - Port{"udp", 5354, ""}, - Port{"udp", 5453, ""}, - Port{"udp", 5553, ""}, + Port{"udp", 104, ""}, + Port{"udp", 106, ""}, + Port{"udp", 146, ""}, Port{"tcp", 8185, ""}, // but not 8186, 8187, 8188 on localhost - Port{"udp", 9353, "iTunes"}, } - pl := appendParsePortsNetstat(nil, netstatOutput) + pl, err := appendParsePortsNetstat(nil, bufio.NewReader(strings.NewReader(netstatOutput))) + if err != nil { + t.Fatal(err) + } + pl = sortAndDedup(pl) jgot, _ := json.MarshalIndent(pl, "", "\t") jwant, _ := json.MarshalIndent(want, "", "\t") if len(pl) != len(want) { @@ -93,7 +74,7 @@ func TestParsePortsNetstat(t *testing.T) { } for i := range pl { if pl[i] != want[i] { - t.Errorf("row#%d\n got: %#v\n\nwant: %#v\n", + t.Errorf("row#%d\n got: %+v\n\nwant: %+v\n", i, pl[i], want[i]) t.Fatalf("Got:\n%s\n\nWant:\n%s\n", jgot, jwant) } diff --git a/portlist/poller.go b/portlist/poller.go index 3476b4075..5dc99e49c 100644 --- a/portlist/poller.go +++ b/portlist/poller.go @@ -10,14 +10,15 @@ package portlist import ( "context" "errors" - "fmt" + "runtime" "sync" "time" "tailscale.com/envknob" - "tailscale.com/version" ) +var pollInterval = 5 * time.Second // default; changed by some OS-specific init funcs + var debugDisablePortlist = envknob.RegisterBool("TS_DEBUG_DISABLE_PORTLIST") // Poller scans the systems for listening ports periodically and sends @@ -29,11 +30,8 @@ type Poller struct { // code. When non-nil, it's responsible for getting the complete list of // cached ports complete with the process name. That is, when set, // addProcesses is not used. - // - // This is part of a multi-step migration (starting 2022-10-22) to move to - // using osImpl for all of Linux, macOS (unsandboxed), and Windows. But - // during the transition period, we support this being nil. - // TODO(bradfitz): finish that migration. + // A nil values means we don't have code for getting the list on the current + // operating system. os osImpl osOnce sync.Once // guards init of os @@ -66,12 +64,11 @@ type osImpl interface { // newOSImpl, if non-nil, constructs a new osImpl. var newOSImpl func() osImpl +var errUnimplemented = errors.New("portlist poller not implemented on " + runtime.GOOS) + // NewPoller returns a new portlist Poller. It returns an error // if the portlist couldn't be obtained. func NewPoller() (*Poller, error) { - if version.OS() == "iOS" { - return nil, errors.New("not available on iOS") - } if debugDisablePortlist() { return nil, errors.New("portlist disabled by envknob") } @@ -81,6 +78,9 @@ func NewPoller() (*Poller, error) { } p.closeCtx, p.closeCtxCancel = context.WithCancel(context.Background()) p.osOnce.Do(p.initOSField) + if p.os == nil { + return nil, errUnimplemented + } // Do one initial poll synchronously so we can return an error // early. @@ -172,25 +172,6 @@ func (p *Poller) getList() (List, error) { } p.osOnce.Do(p.initOSField) var err error - if p.os != nil { - p.scratch, err = p.os.AppendListeningPorts(p.scratch[:0]) - return p.scratch, err - } - - // Old path for OSes that don't have osImpl yet. - // TODO(bradfitz): delete these when macOS and Windows are converted. - p.scratch, err = appendListeningPorts(p.scratch[:0]) - if err != nil { - return nil, fmt.Errorf("listPorts: %s", err) - } - pl := sortAndDedup(p.scratch) - if pl.equal(p.prev) { - // Nothing changed, skip inode lookup - return p.prev, nil - } - pl, err = addProcesses(pl) - if err != nil { - return nil, fmt.Errorf("addProcesses: %s", err) - } - return pl, nil + p.scratch, err = p.os.AppendListeningPorts(p.scratch[:0]) + return p.scratch, err } diff --git a/portlist/portlist_ios.go b/portlist/portlist_ios.go deleted file mode 100644 index cb60054fd..000000000 --- a/portlist/portlist_ios.go +++ /dev/null @@ -1,22 +0,0 @@ -// 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 ios - -package portlist - -import ( - "errors" - "time" -) - -const pollInterval = 9999 * time.Hour - -func appendListeningPorts(base []Port) ([]Port, error) { - return nil, errors.New("not implemented") -} - -func addProcesses(pl []Port) ([]Port, error) { - return nil, errors.New("not implemented") -} diff --git a/portlist/portlist_js.go b/portlist/portlist_js.go deleted file mode 100644 index 3f4ce6a76..000000000 --- a/portlist/portlist_js.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) 2021 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. - -package portlist - -import "time" - -const pollInterval = 365 * 24 * time.Hour - -func appendListeningPorts(base []Port) ([]Port, error) { - return base, nil -} - -func addProcesses(pl []Port) ([]Port, error) { - return pl, nil -} diff --git a/portlist/portlist_linux.go b/portlist/portlist_linux.go index 6ddd1c662..8d9176cd9 100644 --- a/portlist/portlist_linux.go +++ b/portlist/portlist_linux.go @@ -27,6 +27,8 @@ import ( func init() { newOSImpl = newLinuxImpl + // Reading the sockfiles on Linux is very fast, so we can do it often. + pollInterval = 1 * time.Second } type linuxImpl struct { @@ -78,9 +80,6 @@ func (li *linuxImpl) Close() error { return nil } -// Reading the sockfiles on Linux is very fast, so we can do it often. -const pollInterval = 1 * time.Second - const ( v6Localhost = "00000000000000000000000001000000:" v6Any = "00000000000000000000000000000000:0000" @@ -420,11 +419,3 @@ func readlink(path, buf []byte) (n int, ok bool) { } return n, true } - -func appendListeningPorts([]Port) ([]Port, error) { - panic("unused on linux; needed to compile for now") -} - -func addProcesses([]Port) ([]Port, error) { - panic("unused on linux; needed to compile for now") -} diff --git a/portlist/portlist_macos.go b/portlist/portlist_macos.go index eabc58428..a08678c87 100644 --- a/portlist/portlist_macos.go +++ b/portlist/portlist_macos.go @@ -15,16 +15,115 @@ import ( "strings" "sync/atomic" "time" + + "go4.org/mem" ) -// We have to run netstat, which is a bit expensive, so don't do it too often. -const pollInterval = 5 * time.Second +func init() { + newOSImpl = newMacOSImpl + + // We have to run netstat, which is a bit expensive, so don't do it too often. + pollInterval = 5 * time.Second +} + +type macOSImpl struct { + known map[protoPort]*portMeta // inode string => metadata + netstatPath string // lazily populated + + br *bufio.Reader // reused + portsBuf []Port +} + +type protoPort struct { + proto string + port uint16 +} + +type portMeta struct { + port Port + keep bool +} -func appendListeningPorts(base []Port) ([]Port, error) { - return appendListeningPortsNetstat(base, "-na") +func newMacOSImpl() osImpl { + return &macOSImpl{ + known: map[protoPort]*portMeta{}, + br: bufio.NewReader(bytes.NewReader(nil)), + } } -var lsofFailed int64 // atomic bool +func (*macOSImpl) Close() error { return nil } + +func (im *macOSImpl) AppendListeningPorts(base []Port) ([]Port, error) { + var err error + im.portsBuf, err = im.appendListeningPortsNetstat(im.portsBuf[:0]) + if err != nil { + return nil, err + } + + for _, pm := range im.known { + pm.keep = false + } + + var needProcs bool + for _, p := range im.portsBuf { + fp := protoPort{ + proto: p.Proto, + port: p.Port, + } + if pm, ok := im.known[fp]; ok { + pm.keep = true + } else { + needProcs = true + im.known[fp] = &portMeta{ + port: p, + keep: true, + } + } + } + + ret := base + for k, m := range im.known { + if !m.keep { + delete(im.known, k) + } + } + + if needProcs { + im.addProcesses() // best effort + } + + for _, m := range im.known { + ret = append(ret, m.port) + } + return sortAndDedup(ret), nil +} + +func (im *macOSImpl) appendListeningPortsNetstat(base []Port) ([]Port, error) { + if im.netstatPath == "" { + var err error + im.netstatPath, err = exec.LookPath("netstat") + if err != nil { + return nil, fmt.Errorf("netstat: lookup: %v", err) + } + } + + cmd := exec.Command(im.netstatPath, "-na") + outPipe, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + im.br.Reset(outPipe) + + if err := cmd.Start(); err != nil { + return nil, err + } + defer cmd.Process.Wait() + defer cmd.Process.Kill() + + return appendParsePortsNetstat(base, im.br) +} + +var lsofFailed atomic.Bool // In theory, lsof could replace the function of both listPorts() and // addProcesses(), since it provides a superset of the netstat output. @@ -34,75 +133,82 @@ var lsofFailed int64 // atomic bool // 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 { +func (im *macOSImpl) addProcesses() error { + if lsofFailed.Load() { // This previously failed in the macOS sandbox, so don't try again. - return pl, nil + return nil } exe, err := exec.LookPath("lsof") if err != nil { - return nil, fmt.Errorf("lsof: lookup: %v", err) + return fmt.Errorf("lsof: lookup: %v", err) } - output, err := exec.Command(exe, "-F", "-n", "-P", "-O", "-S2", "-T", "-i4", "-i6").Output() + lsofCmd := exec.Command(exe, "-F", "-n", "-P", "-O", "-S2", "-T", "-i4", "-i6") + outPipe, err := lsofCmd.StdoutPipe() + if err != nil { + return err + } + err = lsofCmd.Start() 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) { + if lsofFailed.CompareAndSwap(false, true) { 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 + return 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) + im.br.Reset(outPipe) var cmd, proto string - for scanner.Scan() { - line := scanner.Text() - if line == "" { + for { + line, err := im.br.ReadBytes('\n') + if err != nil { + break + } + if len(line) < 1 { continue } - field, val := line[0], line[1:] + field, val := line[0], bytes.TrimSpace(line[1:]) switch field { case 'p': // starting a new process cmd = "" proto = "" case 'c': - cmd = val + cmd = string(val) // TODO(bradfitz): avoid garbage; cache process names between runs? case 'P': - proto = strings.ToLower(val) + proto = lsofProtoLower(val) case 'n': - if strings.Contains(val, "->") { + if mem.Contains(mem.B(val), mem.S("->")) { 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 - } + port := parsePort(mem.B(val)) + if port <= 0 { + continue + } + pp := protoPort{proto, uint16(port)} + m := im.known[pp] + switch { + case m != nil: + m.port.Process = cmd + default: + // ignore: processes and ports come and go } } } - return pl, nil + return nil +} + +func lsofProtoLower(p []byte) string { + if string(p) == "TCP" { + return "tcp" + } + if string(p) == "UDP" { + return "udp" + } + return strings.ToLower(string(p)) } diff --git a/portlist/portlist_other.go b/portlist/portlist_other.go deleted file mode 100644 index 12925ebfc..000000000 --- a/portlist/portlist_other.go +++ /dev/null @@ -1,22 +0,0 @@ -// 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 !linux && !windows && !darwin && !js - -package portlist - -import "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") -} - -func addProcesses(pl []Port) ([]Port, error) { - // Generic version has no way to get process mappings. - // This has to be OS-specific. - return pl, nil -} diff --git a/portlist/portlist_windows.go b/portlist/portlist_windows.go index 0881ff6ab..811e4f0aa 100644 --- a/portlist/portlist_windows.go +++ b/portlist/portlist_windows.go @@ -14,11 +14,13 @@ import ( "tailscale.com/net/netstat" ) -// Forking on Windows is insanely expensive, so don't do it too often. -const pollInterval = 5 * time.Second - func init() { newOSImpl = newWindowsImpl + // The portlist poller used to fork on Windows, which is insanely expensive, + // so historically we only did this every 5 seconds on Windows. Maybe we + // could reduce it down to 1 seconds like Linux, but nobody's benchmarked as + // of 2022-11-04. + pollInterval = 5 * time.Second } type famPort struct { @@ -116,11 +118,3 @@ func procNameOfPid(pid int) string { name = strings.TrimSuffix(name, ".EXE") return name } - -func appendListeningPorts([]Port) ([]Port, error) { - panic("unused on windows; needed to compile for now") -} - -func addProcesses([]Port) ([]Port, error) { - panic("unused on windows; needed to compile for now") -}