Просмотр исходного кода

types/result, util/lineiter: add package for a result type, use it

This adds a new generic result type (motivated by golang/go#70084) to
try it out, and uses it in the new lineutil package (replacing the old
lineread package), changing that package to return iterators:
sometimes over []byte (when the input is all in memory), but sometimes
iterators over results of []byte, if errors might happen at runtime.

Updates #12912
Updates golang/go#70084

Change-Id: Iacdc1070e661b5fb163907b1e8b07ac7d51d3f83
Signed-off-by: Brad Fitzpatrick <[email protected]>
Brad Fitzpatrick 1 год назад
Родитель
Сommit
01185e436f

+ 2 - 1
cmd/derper/depaware.txt

@@ -140,6 +140,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
         tailscale.com/types/persist                                  from tailscale.com/ipn
         tailscale.com/types/persist                                  from tailscale.com/ipn
         tailscale.com/types/preftype                                 from tailscale.com/ipn
         tailscale.com/types/preftype                                 from tailscale.com/ipn
         tailscale.com/types/ptr                                      from tailscale.com/hostinfo+
         tailscale.com/types/ptr                                      from tailscale.com/hostinfo+
+        tailscale.com/types/result                                   from tailscale.com/util/lineiter
         tailscale.com/types/structs                                  from tailscale.com/ipn+
         tailscale.com/types/structs                                  from tailscale.com/ipn+
         tailscale.com/types/tkatype                                  from tailscale.com/client/tailscale+
         tailscale.com/types/tkatype                                  from tailscale.com/client/tailscale+
         tailscale.com/types/views                                    from tailscale.com/ipn+
         tailscale.com/types/views                                    from tailscale.com/ipn+
@@ -154,7 +155,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
         tailscale.com/util/fastuuid                                  from tailscale.com/tsweb
         tailscale.com/util/fastuuid                                  from tailscale.com/tsweb
      💣 tailscale.com/util/hashx                                     from tailscale.com/util/deephash
      💣 tailscale.com/util/hashx                                     from tailscale.com/util/deephash
         tailscale.com/util/httpm                                     from tailscale.com/client/tailscale
         tailscale.com/util/httpm                                     from tailscale.com/client/tailscale
-        tailscale.com/util/lineread                                  from tailscale.com/hostinfo+
+        tailscale.com/util/lineiter                                  from tailscale.com/hostinfo+
    L    tailscale.com/util/linuxfw                                   from tailscale.com/net/netns
    L    tailscale.com/util/linuxfw                                   from tailscale.com/net/netns
         tailscale.com/util/mak                                       from tailscale.com/health+
         tailscale.com/util/mak                                       from tailscale.com/health+
         tailscale.com/util/multierr                                  from tailscale.com/health+
         tailscale.com/util/multierr                                  from tailscale.com/health+

+ 2 - 1
cmd/k8s-operator/depaware.txt

@@ -775,6 +775,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
         tailscale.com/types/persist                                  from tailscale.com/control/controlclient+
         tailscale.com/types/persist                                  from tailscale.com/control/controlclient+
         tailscale.com/types/preftype                                 from tailscale.com/ipn+
         tailscale.com/types/preftype                                 from tailscale.com/ipn+
         tailscale.com/types/ptr                                      from tailscale.com/cmd/k8s-operator+
         tailscale.com/types/ptr                                      from tailscale.com/cmd/k8s-operator+
+        tailscale.com/types/result                                   from tailscale.com/util/lineiter
         tailscale.com/types/structs                                  from tailscale.com/control/controlclient+
         tailscale.com/types/structs                                  from tailscale.com/control/controlclient+
         tailscale.com/types/tkatype                                  from tailscale.com/client/tailscale+
         tailscale.com/types/tkatype                                  from tailscale.com/client/tailscale+
         tailscale.com/types/views                                    from tailscale.com/appc+
         tailscale.com/types/views                                    from tailscale.com/appc+
@@ -792,7 +793,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
      💣 tailscale.com/util/hashx                                     from tailscale.com/util/deephash
      💣 tailscale.com/util/hashx                                     from tailscale.com/util/deephash
         tailscale.com/util/httphdr                                   from tailscale.com/ipn/ipnlocal+
         tailscale.com/util/httphdr                                   from tailscale.com/ipn/ipnlocal+
         tailscale.com/util/httpm                                     from tailscale.com/client/tailscale+
         tailscale.com/util/httpm                                     from tailscale.com/client/tailscale+
-        tailscale.com/util/lineread                                  from tailscale.com/hostinfo+
+        tailscale.com/util/lineiter                                  from tailscale.com/hostinfo+
    L    tailscale.com/util/linuxfw                                   from tailscale.com/net/netns+
    L    tailscale.com/util/linuxfw                                   from tailscale.com/net/netns+
         tailscale.com/util/mak                                       from tailscale.com/appc+
         tailscale.com/util/mak                                       from tailscale.com/appc+
         tailscale.com/util/multierr                                  from tailscale.com/control/controlclient+
         tailscale.com/util/multierr                                  from tailscale.com/control/controlclient+

+ 2 - 1
cmd/stund/depaware.txt

@@ -67,6 +67,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
         tailscale.com/types/logger                                   from tailscale.com/tsweb
         tailscale.com/types/logger                                   from tailscale.com/tsweb
         tailscale.com/types/opt                                      from tailscale.com/envknob+
         tailscale.com/types/opt                                      from tailscale.com/envknob+
         tailscale.com/types/ptr                                      from tailscale.com/tailcfg+
         tailscale.com/types/ptr                                      from tailscale.com/tailcfg+
+        tailscale.com/types/result                                   from tailscale.com/util/lineiter
         tailscale.com/types/structs                                  from tailscale.com/tailcfg+
         tailscale.com/types/structs                                  from tailscale.com/tailcfg+
         tailscale.com/types/tkatype                                  from tailscale.com/tailcfg+
         tailscale.com/types/tkatype                                  from tailscale.com/tailcfg+
         tailscale.com/types/views                                    from tailscale.com/net/tsaddr+
         tailscale.com/types/views                                    from tailscale.com/net/tsaddr+
@@ -74,7 +75,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
    L 💣 tailscale.com/util/dirwalk                                   from tailscale.com/metrics
    L 💣 tailscale.com/util/dirwalk                                   from tailscale.com/metrics
         tailscale.com/util/dnsname                                   from tailscale.com/tailcfg
         tailscale.com/util/dnsname                                   from tailscale.com/tailcfg
         tailscale.com/util/fastuuid                                  from tailscale.com/tsweb
         tailscale.com/util/fastuuid                                  from tailscale.com/tsweb
-        tailscale.com/util/lineread                                  from tailscale.com/version/distro
+        tailscale.com/util/lineiter                                  from tailscale.com/version/distro
         tailscale.com/util/nocasemaps                                from tailscale.com/types/ipproto
         tailscale.com/util/nocasemaps                                from tailscale.com/types/ipproto
         tailscale.com/util/slicesx                                   from tailscale.com/tailcfg
         tailscale.com/util/slicesx                                   from tailscale.com/tailcfg
         tailscale.com/util/vizerror                                  from tailscale.com/tailcfg+
         tailscale.com/util/vizerror                                  from tailscale.com/tailcfg+

+ 2 - 1
cmd/tailscale/depaware.txt

@@ -148,6 +148,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
         tailscale.com/types/persist                                  from tailscale.com/ipn
         tailscale.com/types/persist                                  from tailscale.com/ipn
         tailscale.com/types/preftype                                 from tailscale.com/cmd/tailscale/cli+
         tailscale.com/types/preftype                                 from tailscale.com/cmd/tailscale/cli+
         tailscale.com/types/ptr                                      from tailscale.com/hostinfo+
         tailscale.com/types/ptr                                      from tailscale.com/hostinfo+
+        tailscale.com/types/result                                   from tailscale.com/util/lineiter
         tailscale.com/types/structs                                  from tailscale.com/ipn+
         tailscale.com/types/structs                                  from tailscale.com/ipn+
         tailscale.com/types/tkatype                                  from tailscale.com/types/key+
         tailscale.com/types/tkatype                                  from tailscale.com/types/key+
         tailscale.com/types/views                                    from tailscale.com/tailcfg+
         tailscale.com/types/views                                    from tailscale.com/tailcfg+
@@ -162,7 +163,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
         tailscale.com/util/groupmember                               from tailscale.com/client/web
         tailscale.com/util/groupmember                               from tailscale.com/client/web
      💣 tailscale.com/util/hashx                                     from tailscale.com/util/deephash
      💣 tailscale.com/util/hashx                                     from tailscale.com/util/deephash
         tailscale.com/util/httpm                                     from tailscale.com/client/tailscale+
         tailscale.com/util/httpm                                     from tailscale.com/client/tailscale+
-        tailscale.com/util/lineread                                  from tailscale.com/hostinfo+
+        tailscale.com/util/lineiter                                  from tailscale.com/hostinfo+
    L    tailscale.com/util/linuxfw                                   from tailscale.com/net/netns
    L    tailscale.com/util/linuxfw                                   from tailscale.com/net/netns
         tailscale.com/util/mak                                       from tailscale.com/cmd/tailscale/cli+
         tailscale.com/util/mak                                       from tailscale.com/cmd/tailscale/cli+
         tailscale.com/util/multierr                                  from tailscale.com/control/controlhttp+
         tailscale.com/util/multierr                                  from tailscale.com/control/controlhttp+

+ 2 - 1
cmd/tailscaled/depaware.txt

@@ -364,6 +364,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
         tailscale.com/types/persist                                  from tailscale.com/control/controlclient+
         tailscale.com/types/persist                                  from tailscale.com/control/controlclient+
         tailscale.com/types/preftype                                 from tailscale.com/ipn+
         tailscale.com/types/preftype                                 from tailscale.com/ipn+
         tailscale.com/types/ptr                                      from tailscale.com/control/controlclient+
         tailscale.com/types/ptr                                      from tailscale.com/control/controlclient+
+        tailscale.com/types/result                                   from tailscale.com/util/lineiter
         tailscale.com/types/structs                                  from tailscale.com/control/controlclient+
         tailscale.com/types/structs                                  from tailscale.com/control/controlclient+
         tailscale.com/types/tkatype                                  from tailscale.com/tka+
         tailscale.com/types/tkatype                                  from tailscale.com/tka+
         tailscale.com/types/views                                    from tailscale.com/ipn/ipnlocal+
         tailscale.com/types/views                                    from tailscale.com/ipn/ipnlocal+
@@ -381,7 +382,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
      💣 tailscale.com/util/hashx                                     from tailscale.com/util/deephash
      💣 tailscale.com/util/hashx                                     from tailscale.com/util/deephash
         tailscale.com/util/httphdr                                   from tailscale.com/ipn/ipnlocal+
         tailscale.com/util/httphdr                                   from tailscale.com/ipn/ipnlocal+
         tailscale.com/util/httpm                                     from tailscale.com/client/tailscale+
         tailscale.com/util/httpm                                     from tailscale.com/client/tailscale+
-        tailscale.com/util/lineread                                  from tailscale.com/hostinfo+
+        tailscale.com/util/lineiter                                  from tailscale.com/hostinfo+
    L    tailscale.com/util/linuxfw                                   from tailscale.com/net/netns+
    L    tailscale.com/util/linuxfw                                   from tailscale.com/net/netns+
         tailscale.com/util/mak                                       from tailscale.com/control/controlclient+
         tailscale.com/util/mak                                       from tailscale.com/control/controlclient+
         tailscale.com/util/multierr                                  from tailscale.com/cmd/tailscaled+
         tailscale.com/util/multierr                                  from tailscale.com/cmd/tailscaled+

+ 12 - 12
hostinfo/hostinfo.go

@@ -25,7 +25,7 @@ import (
 	"tailscale.com/types/ptr"
 	"tailscale.com/types/ptr"
 	"tailscale.com/util/cloudenv"
 	"tailscale.com/util/cloudenv"
 	"tailscale.com/util/dnsname"
 	"tailscale.com/util/dnsname"
-	"tailscale.com/util/lineread"
+	"tailscale.com/util/lineiter"
 	"tailscale.com/version"
 	"tailscale.com/version"
 	"tailscale.com/version/distro"
 	"tailscale.com/version/distro"
 )
 )
@@ -231,12 +231,12 @@ func desktop() (ret opt.Bool) {
 	}
 	}
 
 
 	seenDesktop := false
 	seenDesktop := false
-	lineread.File("/proc/net/unix", func(line []byte) error {
+	for lr := range lineiter.File("/proc/net/unix") {
+		line, _ := lr.Value()
 		seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S(" @/tmp/dbus-"))
 		seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S(" @/tmp/dbus-"))
 		seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S(".X11-unix"))
 		seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S(".X11-unix"))
 		seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S("/wayland-1"))
 		seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S("/wayland-1"))
-		return nil
-	})
+	}
 	ret.Set(seenDesktop)
 	ret.Set(seenDesktop)
 
 
 	// Only cache after a minute - compositors might not have started yet.
 	// Only cache after a minute - compositors might not have started yet.
@@ -305,21 +305,21 @@ func inContainer() opt.Bool {
 		ret.Set(true)
 		ret.Set(true)
 		return ret
 		return ret
 	}
 	}
-	lineread.File("/proc/1/cgroup", func(line []byte) error {
+	for lr := range lineiter.File("/proc/1/cgroup") {
+		line, _ := lr.Value()
 		if mem.Contains(mem.B(line), mem.S("/docker/")) ||
 		if mem.Contains(mem.B(line), mem.S("/docker/")) ||
 			mem.Contains(mem.B(line), mem.S("/lxc/")) {
 			mem.Contains(mem.B(line), mem.S("/lxc/")) {
 			ret.Set(true)
 			ret.Set(true)
-			return io.EOF // arbitrary non-nil error to stop loop
+			break
 		}
 		}
-		return nil
-	})
-	lineread.File("/proc/mounts", func(line []byte) error {
+	}
+	for lr := range lineiter.File("/proc/mounts") {
+		line, _ := lr.Value()
 		if mem.Contains(mem.B(line), mem.S("lxcfs /proc/cpuinfo fuse.lxcfs")) {
 		if mem.Contains(mem.B(line), mem.S("lxcfs /proc/cpuinfo fuse.lxcfs")) {
 			ret.Set(true)
 			ret.Set(true)
-			return io.EOF
+			break
 		}
 		}
-		return nil
-	})
+	}
 	return ret
 	return ret
 }
 }
 
 

+ 8 - 5
hostinfo/hostinfo_linux.go

@@ -12,7 +12,7 @@ import (
 
 
 	"golang.org/x/sys/unix"
 	"golang.org/x/sys/unix"
 	"tailscale.com/types/ptr"
 	"tailscale.com/types/ptr"
-	"tailscale.com/util/lineread"
+	"tailscale.com/util/lineiter"
 	"tailscale.com/version/distro"
 	"tailscale.com/version/distro"
 )
 )
 
 
@@ -106,15 +106,18 @@ func linuxVersionMeta() (meta versionMeta) {
 	}
 	}
 
 
 	m := map[string]string{}
 	m := map[string]string{}
-	lineread.File(propFile, func(line []byte) error {
+	for lr := range lineiter.File(propFile) {
+		line, err := lr.Value()
+		if err != nil {
+			break
+		}
 		eq := bytes.IndexByte(line, '=')
 		eq := bytes.IndexByte(line, '=')
 		if eq == -1 {
 		if eq == -1 {
-			return nil
+			continue
 		}
 		}
 		k, v := string(line[:eq]), strings.Trim(string(line[eq+1:]), `"'`)
 		k, v := string(line[:eq]), strings.Trim(string(line[eq+1:]), `"'`)
 		m[k] = v
 		m[k] = v
-		return nil
-	})
+	}
 
 
 	if v := m["VERSION_CODENAME"]; v != "" {
 	if v := m["VERSION_CODENAME"]; v != "" {
 		meta.DistroCodeName = v
 		meta.DistroCodeName = v

+ 12 - 10
ipn/ipnlocal/ssh.go

@@ -27,7 +27,7 @@ import (
 	"github.com/tailscale/golang-x-crypto/ssh"
 	"github.com/tailscale/golang-x-crypto/ssh"
 	"go4.org/mem"
 	"go4.org/mem"
 	"tailscale.com/tailcfg"
 	"tailscale.com/tailcfg"
-	"tailscale.com/util/lineread"
+	"tailscale.com/util/lineiter"
 	"tailscale.com/util/mak"
 	"tailscale.com/util/mak"
 )
 )
 
 
@@ -80,30 +80,32 @@ func (b *LocalBackend) getSSHUsernames(req *tailcfg.C2NSSHUsernamesRequest) (*ta
 		if err != nil {
 		if err != nil {
 			return nil, err
 			return nil, err
 		}
 		}
-		lineread.Reader(bytes.NewReader(out), func(line []byte) error {
+		for line := range lineiter.Bytes(out) {
 			line = bytes.TrimSpace(line)
 			line = bytes.TrimSpace(line)
 			if len(line) == 0 || line[0] == '_' {
 			if len(line) == 0 || line[0] == '_' {
-				return nil
+				continue
 			}
 			}
 			add(string(line))
 			add(string(line))
-			return nil
-		})
+		}
 	default:
 	default:
-		lineread.File("/etc/passwd", func(line []byte) error {
+		for lr := range lineiter.File("/etc/passwd") {
+			line, err := lr.Value()
+			if err != nil {
+				break
+			}
 			line = bytes.TrimSpace(line)
 			line = bytes.TrimSpace(line)
 			if len(line) == 0 || line[0] == '#' || line[0] == '_' {
 			if len(line) == 0 || line[0] == '#' || line[0] == '_' {
-				return nil
+				continue
 			}
 			}
 			if mem.HasSuffix(mem.B(line), mem.S("/nologin")) ||
 			if mem.HasSuffix(mem.B(line), mem.S("/nologin")) ||
 				mem.HasSuffix(mem.B(line), mem.S("/false")) {
 				mem.HasSuffix(mem.B(line), mem.S("/false")) {
-				return nil
+				continue
 			}
 			}
 			colon := bytes.IndexByte(line, ':')
 			colon := bytes.IndexByte(line, ':')
 			if colon != -1 {
 			if colon != -1 {
 				add(string(line[:colon]))
 				add(string(line[:colon]))
 			}
 			}
-			return nil
-		})
+		}
 	}
 	}
 	return res, nil
 	return res, nil
 }
 }

+ 23 - 28
net/netmon/interfaces_android.go

@@ -5,7 +5,6 @@ package netmon
 
 
 import (
 import (
 	"bytes"
 	"bytes"
-	"errors"
 	"log"
 	"log"
 	"net/netip"
 	"net/netip"
 	"os/exec"
 	"os/exec"
@@ -15,7 +14,7 @@ import (
 	"golang.org/x/sys/unix"
 	"golang.org/x/sys/unix"
 	"tailscale.com/net/netaddr"
 	"tailscale.com/net/netaddr"
 	"tailscale.com/syncs"
 	"tailscale.com/syncs"
-	"tailscale.com/util/lineread"
+	"tailscale.com/util/lineiter"
 )
 )
 
 
 var (
 var (
@@ -34,11 +33,6 @@ func init() {
 
 
 var procNetRouteErr atomic.Bool
 var procNetRouteErr atomic.Bool
 
 
-// errStopReading is a sentinel error value used internally by
-// lineread.File callers to stop reading. It doesn't escape to
-// callers/users.
-var errStopReading = errors.New("stop reading")
-
 /*
 /*
 Parse 10.0.0.1 out of:
 Parse 10.0.0.1 out of:
 
 
@@ -54,44 +48,42 @@ func likelyHomeRouterIPAndroid() (ret netip.Addr, myIP netip.Addr, ok bool) {
 	}
 	}
 	lineNum := 0
 	lineNum := 0
 	var f []mem.RO
 	var f []mem.RO
-	err := lineread.File(procNetRoutePath, func(line []byte) error {
+	for lr := range lineiter.File(procNetRoutePath) {
+		line, err := lr.Value()
+		if err != nil {
+			procNetRouteErr.Store(true)
+			return likelyHomeRouterIP()
+		}
+
 		lineNum++
 		lineNum++
 		if lineNum == 1 {
 		if lineNum == 1 {
 			// Skip header line.
 			// Skip header line.
-			return nil
+			continue
 		}
 		}
 		if lineNum > maxProcNetRouteRead {
 		if lineNum > maxProcNetRouteRead {
-			return errStopReading
+			break
 		}
 		}
 		f = mem.AppendFields(f[:0], mem.B(line))
 		f = mem.AppendFields(f[:0], mem.B(line))
 		if len(f) < 4 {
 		if len(f) < 4 {
-			return nil
+			continue
 		}
 		}
 		gwHex, flagsHex := f[2], f[3]
 		gwHex, flagsHex := f[2], f[3]
 		flags, err := mem.ParseUint(flagsHex, 16, 16)
 		flags, err := mem.ParseUint(flagsHex, 16, 16)
 		if err != nil {
 		if err != nil {
-			return nil // ignore error, skip line and keep going
+			continue // ignore error, skip line and keep going
 		}
 		}
 		if flags&(unix.RTF_UP|unix.RTF_GATEWAY) != unix.RTF_UP|unix.RTF_GATEWAY {
 		if flags&(unix.RTF_UP|unix.RTF_GATEWAY) != unix.RTF_UP|unix.RTF_GATEWAY {
-			return nil
+			continue
 		}
 		}
 		ipu32, err := mem.ParseUint(gwHex, 16, 32)
 		ipu32, err := mem.ParseUint(gwHex, 16, 32)
 		if err != nil {
 		if err != nil {
-			return nil // ignore error, skip line and keep going
+			continue // ignore error, skip line and keep going
 		}
 		}
 		ip := netaddr.IPv4(byte(ipu32), byte(ipu32>>8), byte(ipu32>>16), byte(ipu32>>24))
 		ip := netaddr.IPv4(byte(ipu32), byte(ipu32>>8), byte(ipu32>>16), byte(ipu32>>24))
 		if ip.IsPrivate() {
 		if ip.IsPrivate() {
 			ret = ip
 			ret = ip
-			return errStopReading
+			break
 		}
 		}
-		return nil
-	})
-	if errors.Is(err, errStopReading) {
-		err = nil
-	}
-	if err != nil {
-		procNetRouteErr.Store(true)
-		return likelyHomeRouterIP()
 	}
 	}
 	if ret.IsValid() {
 	if ret.IsValid() {
 		// Try to get the local IP of the interface associated with
 		// Try to get the local IP of the interface associated with
@@ -144,23 +136,26 @@ func likelyHomeRouterIPHelper() (ret netip.Addr, _ netip.Addr, ok bool) {
 		return
 		return
 	}
 	}
 	// Search for line like "default via 10.0.2.2 dev radio0 table 1016 proto static mtu 1500 "
 	// Search for line like "default via 10.0.2.2 dev radio0 table 1016 proto static mtu 1500 "
-	lineread.Reader(out, func(line []byte) error {
+	for lr := range lineiter.Reader(out) {
+		line, err := lr.Value()
+		if err != nil {
+			break
+		}
 		const pfx = "default via "
 		const pfx = "default via "
 		if !mem.HasPrefix(mem.B(line), mem.S(pfx)) {
 		if !mem.HasPrefix(mem.B(line), mem.S(pfx)) {
-			return nil
+			continue
 		}
 		}
 		line = line[len(pfx):]
 		line = line[len(pfx):]
 		sp := bytes.IndexByte(line, ' ')
 		sp := bytes.IndexByte(line, ' ')
 		if sp == -1 {
 		if sp == -1 {
-			return nil
+			continue
 		}
 		}
 		ipb := line[:sp]
 		ipb := line[:sp]
 		if ip, err := netip.ParseAddr(string(ipb)); err == nil && ip.Is4() {
 		if ip, err := netip.ParseAddr(string(ipb)); err == nil && ip.Is4() {
 			ret = ip
 			ret = ip
 			log.Printf("interfaces: found Android default route %v", ip)
 			log.Printf("interfaces: found Android default route %v", ip)
 		}
 		}
-		return nil
-	})
+	}
 	cmd.Process.Kill()
 	cmd.Process.Kill()
 	cmd.Wait()
 	cmd.Wait()
 	return ret, netip.Addr{}, ret.IsValid()
 	return ret, netip.Addr{}, ret.IsValid()

+ 12 - 12
net/netmon/interfaces_darwin_test.go

@@ -4,14 +4,13 @@
 package netmon
 package netmon
 
 
 import (
 import (
-	"errors"
 	"io"
 	"io"
 	"net/netip"
 	"net/netip"
 	"os/exec"
 	"os/exec"
 	"testing"
 	"testing"
 
 
 	"go4.org/mem"
 	"go4.org/mem"
-	"tailscale.com/util/lineread"
+	"tailscale.com/util/lineiter"
 	"tailscale.com/version"
 	"tailscale.com/version"
 )
 )
 
 
@@ -73,31 +72,34 @@ func likelyHomeRouterIPDarwinExec() (ret netip.Addr, netif string, ok bool) {
 	defer io.Copy(io.Discard, stdout) // clear the pipe to prevent hangs
 	defer io.Copy(io.Discard, stdout) // clear the pipe to prevent hangs
 
 
 	var f []mem.RO
 	var f []mem.RO
-	lineread.Reader(stdout, func(lineb []byte) error {
+	for lr := range lineiter.Reader(stdout) {
+		lineb, err := lr.Value()
+		if err != nil {
+			break
+		}
 		line := mem.B(lineb)
 		line := mem.B(lineb)
 		if !mem.Contains(line, mem.S("default")) {
 		if !mem.Contains(line, mem.S("default")) {
-			return nil
+			continue
 		}
 		}
 		f = mem.AppendFields(f[:0], line)
 		f = mem.AppendFields(f[:0], line)
 		if len(f) < 4 || !f[0].EqualString("default") {
 		if len(f) < 4 || !f[0].EqualString("default") {
-			return nil
+			continue
 		}
 		}
 		ipm, flagsm, netifm := f[1], f[2], f[3]
 		ipm, flagsm, netifm := f[1], f[2], f[3]
 		if !mem.Contains(flagsm, mem.S("G")) {
 		if !mem.Contains(flagsm, mem.S("G")) {
-			return nil
+			continue
 		}
 		}
 		if mem.Contains(flagsm, mem.S("I")) {
 		if mem.Contains(flagsm, mem.S("I")) {
-			return nil
+			continue
 		}
 		}
 		ip, err := netip.ParseAddr(string(mem.Append(nil, ipm)))
 		ip, err := netip.ParseAddr(string(mem.Append(nil, ipm)))
 		if err == nil && ip.IsPrivate() {
 		if err == nil && ip.IsPrivate() {
 			ret = ip
 			ret = ip
 			netif = netifm.StringCopy()
 			netif = netifm.StringCopy()
 			// We've found what we're looking for.
 			// We've found what we're looking for.
-			return errStopReadingNetstatTable
+			break
 		}
 		}
-		return nil
-	})
+	}
 	return ret, netif, ret.IsValid()
 	return ret, netif, ret.IsValid()
 }
 }
 
 
@@ -110,5 +112,3 @@ func TestFetchRoutingTable(t *testing.T) {
 		}
 		}
 	}
 	}
 }
 }
-
-var errStopReadingNetstatTable = errors.New("found private gateway")

+ 15 - 22
net/netmon/interfaces_linux.go

@@ -23,7 +23,7 @@ import (
 	"go4.org/mem"
 	"go4.org/mem"
 	"golang.org/x/sys/unix"
 	"golang.org/x/sys/unix"
 	"tailscale.com/net/netaddr"
 	"tailscale.com/net/netaddr"
-	"tailscale.com/util/lineread"
+	"tailscale.com/util/lineiter"
 )
 )
 
 
 func init() {
 func init() {
@@ -32,11 +32,6 @@ func init() {
 
 
 var procNetRouteErr atomic.Bool
 var procNetRouteErr atomic.Bool
 
 
-// errStopReading is a sentinel error value used internally by
-// lineread.File callers to stop reading. It doesn't escape to
-// callers/users.
-var errStopReading = errors.New("stop reading")
-
 /*
 /*
 Parse 10.0.0.1 out of:
 Parse 10.0.0.1 out of:
 
 
@@ -52,44 +47,42 @@ func likelyHomeRouterIPLinux() (ret netip.Addr, myIP netip.Addr, ok bool) {
 	}
 	}
 	lineNum := 0
 	lineNum := 0
 	var f []mem.RO
 	var f []mem.RO
-	err := lineread.File(procNetRoutePath, func(line []byte) error {
+	for lr := range lineiter.File(procNetRoutePath) {
+		line, err := lr.Value()
+		if err != nil {
+			procNetRouteErr.Store(true)
+			log.Printf("interfaces: failed to read /proc/net/route: %v", err)
+			return ret, myIP, false
+		}
 		lineNum++
 		lineNum++
 		if lineNum == 1 {
 		if lineNum == 1 {
 			// Skip header line.
 			// Skip header line.
-			return nil
+			continue
 		}
 		}
 		if lineNum > maxProcNetRouteRead {
 		if lineNum > maxProcNetRouteRead {
-			return errStopReading
+			break
 		}
 		}
 		f = mem.AppendFields(f[:0], mem.B(line))
 		f = mem.AppendFields(f[:0], mem.B(line))
 		if len(f) < 4 {
 		if len(f) < 4 {
-			return nil
+			continue
 		}
 		}
 		gwHex, flagsHex := f[2], f[3]
 		gwHex, flagsHex := f[2], f[3]
 		flags, err := mem.ParseUint(flagsHex, 16, 16)
 		flags, err := mem.ParseUint(flagsHex, 16, 16)
 		if err != nil {
 		if err != nil {
-			return nil // ignore error, skip line and keep going
+			continue // ignore error, skip line and keep going
 		}
 		}
 		if flags&(unix.RTF_UP|unix.RTF_GATEWAY) != unix.RTF_UP|unix.RTF_GATEWAY {
 		if flags&(unix.RTF_UP|unix.RTF_GATEWAY) != unix.RTF_UP|unix.RTF_GATEWAY {
-			return nil
+			continue
 		}
 		}
 		ipu32, err := mem.ParseUint(gwHex, 16, 32)
 		ipu32, err := mem.ParseUint(gwHex, 16, 32)
 		if err != nil {
 		if err != nil {
-			return nil // ignore error, skip line and keep going
+			continue // ignore error, skip line and keep going
 		}
 		}
 		ip := netaddr.IPv4(byte(ipu32), byte(ipu32>>8), byte(ipu32>>16), byte(ipu32>>24))
 		ip := netaddr.IPv4(byte(ipu32), byte(ipu32>>8), byte(ipu32>>16), byte(ipu32>>24))
 		if ip.IsPrivate() {
 		if ip.IsPrivate() {
 			ret = ip
 			ret = ip
-			return errStopReading
+			break
 		}
 		}
-		return nil
-	})
-	if errors.Is(err, errStopReading) {
-		err = nil
-	}
-	if err != nil {
-		procNetRouteErr.Store(true)
-		log.Printf("interfaces: failed to read /proc/net/route: %v", err)
 	}
 	}
 	if ret.IsValid() {
 	if ret.IsValid() {
 		// Try to get the local IP of the interface associated with
 		// Try to get the local IP of the interface associated with

+ 2 - 0
net/netmon/netmon_linux_test.go

@@ -1,6 +1,8 @@
 // Copyright (c) Tailscale Inc & AUTHORS
 // Copyright (c) Tailscale Inc & AUTHORS
 // SPDX-License-Identifier: BSD-3-Clause
 // SPDX-License-Identifier: BSD-3-Clause
 
 
+//go:build linux && !android
+
 package netmon
 package netmon
 
 
 import (
 import (

+ 8 - 7
net/tshttpproxy/tshttpproxy_synology.go

@@ -17,7 +17,7 @@ import (
 	"sync"
 	"sync"
 	"time"
 	"time"
 
 
-	"tailscale.com/util/lineread"
+	"tailscale.com/util/lineiter"
 )
 )
 
 
 // These vars are overridden for tests.
 // These vars are overridden for tests.
@@ -76,21 +76,22 @@ func synologyProxiesFromConfig() (*url.URL, *url.URL, error) {
 func parseSynologyConfig(r io.Reader) (*url.URL, *url.URL, error) {
 func parseSynologyConfig(r io.Reader) (*url.URL, *url.URL, error) {
 	cfg := map[string]string{}
 	cfg := map[string]string{}
 
 
-	if err := lineread.Reader(r, func(line []byte) error {
+	for lr := range lineiter.Reader(r) {
+		line, err := lr.Value()
+		if err != nil {
+			return nil, nil, err
+		}
 		// accept and skip over empty lines
 		// accept and skip over empty lines
 		line = bytes.TrimSpace(line)
 		line = bytes.TrimSpace(line)
 		if len(line) == 0 {
 		if len(line) == 0 {
-			return nil
+			continue
 		}
 		}
 
 
 		key, value, ok := strings.Cut(string(line), "=")
 		key, value, ok := strings.Cut(string(line), "=")
 		if !ok {
 		if !ok {
-			return fmt.Errorf("missing \"=\" in proxy.conf line: %q", line)
+			return nil, nil, fmt.Errorf("missing \"=\" in proxy.conf line: %q", line)
 		}
 		}
 		cfg[string(key)] = string(value)
 		cfg[string(key)] = string(value)
-		return nil
-	}); err != nil {
-		return nil, nil, err
 	}
 	}
 
 
 	if cfg["proxy_enabled"] != "yes" {
 	if cfg["proxy_enabled"] != "yes" {

+ 5 - 8
ssh/tailssh/tailssh_test.go

@@ -48,7 +48,7 @@ import (
 	"tailscale.com/types/netmap"
 	"tailscale.com/types/netmap"
 	"tailscale.com/types/ptr"
 	"tailscale.com/types/ptr"
 	"tailscale.com/util/cibuild"
 	"tailscale.com/util/cibuild"
-	"tailscale.com/util/lineread"
+	"tailscale.com/util/lineiter"
 	"tailscale.com/util/must"
 	"tailscale.com/util/must"
 	"tailscale.com/version/distro"
 	"tailscale.com/version/distro"
 	"tailscale.com/wgengine"
 	"tailscale.com/wgengine"
@@ -1123,14 +1123,11 @@ func TestSSH(t *testing.T) {
 
 
 func parseEnv(out []byte) map[string]string {
 func parseEnv(out []byte) map[string]string {
 	e := map[string]string{}
 	e := map[string]string{}
-	lineread.Reader(bytes.NewReader(out), func(line []byte) error {
-		i := bytes.IndexByte(line, '=')
-		if i == -1 {
-			return nil
+	for line := range lineiter.Bytes(out) {
+		if i := bytes.IndexByte(line, '='); i != -1 {
+			e[string(line[:i])] = string(line[i+1:])
 		}
 		}
-		e[string(line[:i])] = string(line[i+1:])
-		return nil
-	})
+	}
 	return e
 	return e
 }
 }
 
 

+ 9 - 9
ssh/tailssh/user.go

@@ -6,7 +6,6 @@
 package tailssh
 package tailssh
 
 
 import (
 import (
-	"io"
 	"os"
 	"os"
 	"os/exec"
 	"os/exec"
 	"os/user"
 	"os/user"
@@ -18,7 +17,7 @@ import (
 	"go4.org/mem"
 	"go4.org/mem"
 	"tailscale.com/envknob"
 	"tailscale.com/envknob"
 	"tailscale.com/hostinfo"
 	"tailscale.com/hostinfo"
-	"tailscale.com/util/lineread"
+	"tailscale.com/util/lineiter"
 	"tailscale.com/util/osuser"
 	"tailscale.com/util/osuser"
 	"tailscale.com/version/distro"
 	"tailscale.com/version/distro"
 )
 )
@@ -110,15 +109,16 @@ func defaultPathForUser(u *user.User) string {
 }
 }
 
 
 func defaultPathForUserOnNixOS(u *user.User) string {
 func defaultPathForUserOnNixOS(u *user.User) string {
-	var path string
-	lineread.File("/etc/pam/environment", func(lineb []byte) error {
+	for lr := range lineiter.File("/etc/pam/environment") {
+		lineb, err := lr.Value()
+		if err != nil {
+			return ""
+		}
 		if v := pathFromPAMEnvLine(lineb, u); v != "" {
 		if v := pathFromPAMEnvLine(lineb, u); v != "" {
-			path = v
-			return io.EOF // stop iteration
+			return v
 		}
 		}
-		return nil
-	})
-	return path
+	}
+	return ""
 }
 }
 
 
 func pathFromPAMEnvLine(line []byte, u *user.User) (path string) {
 func pathFromPAMEnvLine(line []byte, u *user.User) (path string) {

+ 49 - 0
types/result/result.go

@@ -0,0 +1,49 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package result contains the Of result type, which is
+// either a value or an error.
+package result
+
+// Of is either a T value or an error.
+//
+// Think of it like Rust or Swift's result types.
+// It's named "Of" because the fully qualified name
+// for callers reads result.Of[T].
+type Of[T any] struct {
+	v   T // valid if Err is nil; invalid if Err is non-nil
+	err error
+}
+
+// Value returns a new result with value v,
+// without an error.
+func Value[T any](v T) Of[T] {
+	return Of[T]{v: v}
+}
+
+// Error returns a new result with error err.
+// If err is nil, the returned result is equivalent
+// to calling Value with T's zero value.
+func Error[T any](err error) Of[T] {
+	return Of[T]{err: err}
+}
+
+// MustValue returns r's result value.
+// It panics if r.Err returns non-nil.
+func (r Of[T]) MustValue() T {
+	if r.err != nil {
+		panic(r.err)
+	}
+	return r.v
+}
+
+// Value returns r's result value and error.
+func (r Of[T]) Value() (T, error) {
+	return r.v, r.err
+}
+
+// Err returns r's error, if any.
+// When r.Err returns nil, it's safe to call r.MustValue without it panicking.
+func (r Of[T]) Err() error {
+	return r.err
+}

+ 72 - 0
util/lineiter/lineiter.go

@@ -0,0 +1,72 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package lineiter iterates over lines in things.
+package lineiter
+
+import (
+	"bufio"
+	"bytes"
+	"io"
+	"iter"
+	"os"
+
+	"tailscale.com/types/result"
+)
+
+// File returns an iterator that reads lines from the named file.
+//
+// The returned substrings don't include the trailing newline.
+// Lines may be empty.
+func File(name string) iter.Seq[result.Of[[]byte]] {
+	f, err := os.Open(name)
+	return reader(f, f, err)
+}
+
+// Bytes returns an iterator over the lines in bs.
+// The returned substrings don't include the trailing newline.
+// Lines may be empty.
+func Bytes(bs []byte) iter.Seq[[]byte] {
+	return func(yield func([]byte) bool) {
+		for len(bs) > 0 {
+			i := bytes.IndexByte(bs, '\n')
+			if i < 0 {
+				yield(bs)
+				return
+			}
+			if !yield(bs[:i]) {
+				return
+			}
+			bs = bs[i+1:]
+		}
+	}
+}
+
+// Reader returns an iterator over the lines in r.
+//
+// The returned substrings don't include the trailing newline.
+// Lines may be empty.
+func Reader(r io.Reader) iter.Seq[result.Of[[]byte]] {
+	return reader(r, nil, nil)
+}
+
+func reader(r io.Reader, c io.Closer, err error) iter.Seq[result.Of[[]byte]] {
+	return func(yield func(result.Of[[]byte]) bool) {
+		if err != nil {
+			yield(result.Error[[]byte](err))
+			return
+		}
+		if c != nil {
+			defer c.Close()
+		}
+		bs := bufio.NewScanner(r)
+		for bs.Scan() {
+			if !yield(result.Value(bs.Bytes())) {
+				return
+			}
+		}
+		if err := bs.Err(); err != nil {
+			yield(result.Error[[]byte](err))
+		}
+	}
+}

+ 32 - 0
util/lineiter/lineiter_test.go

@@ -0,0 +1,32 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package lineiter
+
+import (
+	"slices"
+	"strings"
+	"testing"
+)
+
+func TestBytesLines(t *testing.T) {
+	var got []string
+	for line := range Bytes([]byte("foo\n\nbar\nbaz")) {
+		got = append(got, string(line))
+	}
+	want := []string{"foo", "", "bar", "baz"}
+	if !slices.Equal(got, want) {
+		t.Errorf("got %q; want %q", got, want)
+	}
+}
+
+func TestReader(t *testing.T) {
+	var got []string
+	for line := range Reader(strings.NewReader("foo\n\nbar\nbaz")) {
+		got = append(got, string(line.MustValue()))
+	}
+	want := []string{"foo", "", "bar", "baz"}
+	if !slices.Equal(got, want) {
+		t.Errorf("got %q; want %q", got, want)
+	}
+}

+ 10 - 10
util/pidowner/pidowner_linux.go

@@ -8,26 +8,26 @@ import (
 	"os"
 	"os"
 	"strings"
 	"strings"
 
 
-	"tailscale.com/util/lineread"
+	"tailscale.com/util/lineiter"
 )
 )
 
 
 func ownerOfPID(pid int) (userID string, err error) {
 func ownerOfPID(pid int) (userID string, err error) {
 	file := fmt.Sprintf("/proc/%d/status", pid)
 	file := fmt.Sprintf("/proc/%d/status", pid)
-	err = lineread.File(file, func(line []byte) error {
+	for lr := range lineiter.File(file) {
+		line, err := lr.Value()
+		if err != nil {
+			if os.IsNotExist(err) {
+				return "", ErrProcessNotFound
+			}
+			return "", err
+		}
 		if len(line) < 4 || string(line[:4]) != "Uid:" {
 		if len(line) < 4 || string(line[:4]) != "Uid:" {
-			return nil
+			continue
 		}
 		}
 		f := strings.Fields(string(line))
 		f := strings.Fields(string(line))
 		if len(f) >= 2 {
 		if len(f) >= 2 {
 			userID = f[1] // real userid
 			userID = f[1] // real userid
 		}
 		}
-		return nil
-	})
-	if os.IsNotExist(err) {
-		return "", ErrProcessNotFound
-	}
-	if err != nil {
-		return
 	}
 	}
 	if userID == "" {
 	if userID == "" {
 		return "", fmt.Errorf("missing Uid line in %s", file)
 		return "", fmt.Errorf("missing Uid line in %s", file)

+ 10 - 10
version/distro/distro.go

@@ -6,13 +6,12 @@ package distro
 
 
 import (
 import (
 	"bytes"
 	"bytes"
-	"io"
 	"os"
 	"os"
 	"runtime"
 	"runtime"
 	"strconv"
 	"strconv"
 
 
 	"tailscale.com/types/lazy"
 	"tailscale.com/types/lazy"
-	"tailscale.com/util/lineread"
+	"tailscale.com/util/lineiter"
 )
 )
 
 
 type Distro string
 type Distro string
@@ -132,18 +131,19 @@ func DSMVersion() int {
 			return v
 			return v
 		}
 		}
 		// But when run from the command line, we have to read it from the file:
 		// But when run from the command line, we have to read it from the file:
-		lineread.File("/etc/VERSION", func(line []byte) error {
+		for lr := range lineiter.File("/etc/VERSION") {
+			line, err := lr.Value()
+			if err != nil {
+				break // but otherwise ignore
+			}
 			line = bytes.TrimSpace(line)
 			line = bytes.TrimSpace(line)
 			if string(line) == `majorversion="7"` {
 			if string(line) == `majorversion="7"` {
-				v = 7
-				return io.EOF
+				return 7
 			}
 			}
 			if string(line) == `majorversion="6"` {
 			if string(line) == `majorversion="6"` {
-				v = 6
-				return io.EOF
+				return 6
 			}
 			}
-			return nil
-		})
-		return v
+		}
+		return 0
 	})
 	})
 }
 }