M cmd/derper/depaware.txt => cmd/derper/depaware.txt +2 -1
@@ 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/preftype from tailscale.com/ipn
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/tkatype from tailscale.com/client/tailscale+
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/hashx from tailscale.com/util/deephash
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
tailscale.com/util/mak from tailscale.com/health+
tailscale.com/util/multierr from tailscale.com/health+
M cmd/k8s-operator/depaware.txt => cmd/k8s-operator/depaware.txt +2 -1
@@ 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/preftype from tailscale.com/ipn+
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/tkatype from tailscale.com/client/tailscale+
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/httphdr from tailscale.com/ipn/ipnlocal+
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+
tailscale.com/util/mak from tailscale.com/appc+
tailscale.com/util/multierr from tailscale.com/control/controlclient+
M cmd/stund/depaware.txt => cmd/stund/depaware.txt +2 -1
@@ 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/opt from tailscale.com/envknob+
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/tkatype from tailscale.com/tailcfg+
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
tailscale.com/util/dnsname from tailscale.com/tailcfg
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/slicesx from tailscale.com/tailcfg
tailscale.com/util/vizerror from tailscale.com/tailcfg+
M cmd/tailscale/depaware.txt => cmd/tailscale/depaware.txt +2 -1
@@ 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/preftype from tailscale.com/cmd/tailscale/cli+
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/tkatype from tailscale.com/types/key+
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/hashx from tailscale.com/util/deephash
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
tailscale.com/util/mak from tailscale.com/cmd/tailscale/cli+
tailscale.com/util/multierr from tailscale.com/control/controlhttp+
M cmd/tailscaled/depaware.txt => cmd/tailscaled/depaware.txt +2 -1
@@ 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/preftype from tailscale.com/ipn+
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/tkatype from tailscale.com/tka+
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/httphdr from tailscale.com/ipn/ipnlocal+
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+
tailscale.com/util/mak from tailscale.com/control/controlclient+
tailscale.com/util/multierr from tailscale.com/cmd/tailscaled+
M hostinfo/hostinfo.go => hostinfo/hostinfo.go +12 -12
@@ 25,7 25,7 @@ import (
"tailscale.com/types/ptr"
"tailscale.com/util/cloudenv"
"tailscale.com/util/dnsname"
- "tailscale.com/util/lineread"
+ "tailscale.com/util/lineiter"
"tailscale.com/version"
"tailscale.com/version/distro"
)
@@ 231,12 231,12 @@ func desktop() (ret opt.Bool) {
}
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(".X11-unix"))
seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S("/wayland-1"))
- return nil
- })
+ }
ret.Set(seenDesktop)
// Only cache after a minute - compositors might not have started yet.
@@ 305,21 305,21 @@ func inContainer() opt.Bool {
ret.Set(true)
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/")) ||
mem.Contains(mem.B(line), mem.S("/lxc/")) {
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")) {
ret.Set(true)
- return io.EOF
+ break
}
- return nil
- })
+ }
return ret
}
M hostinfo/hostinfo_linux.go => hostinfo/hostinfo_linux.go +8 -5
@@ 12,7 12,7 @@ import (
"golang.org/x/sys/unix"
"tailscale.com/types/ptr"
- "tailscale.com/util/lineread"
+ "tailscale.com/util/lineiter"
"tailscale.com/version/distro"
)
@@ 106,15 106,18 @@ func linuxVersionMeta() (meta versionMeta) {
}
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, '=')
if eq == -1 {
- return nil
+ continue
}
k, v := string(line[:eq]), strings.Trim(string(line[eq+1:]), `"'`)
m[k] = v
- return nil
- })
+ }
if v := m["VERSION_CODENAME"]; v != "" {
meta.DistroCodeName = v
M ipn/ipnlocal/ssh.go => ipn/ipnlocal/ssh.go +12 -10
@@ 27,7 27,7 @@ import (
"github.com/tailscale/golang-x-crypto/ssh"
"go4.org/mem"
"tailscale.com/tailcfg"
- "tailscale.com/util/lineread"
+ "tailscale.com/util/lineiter"
"tailscale.com/util/mak"
)
@@ 80,30 80,32 @@ func (b *LocalBackend) getSSHUsernames(req *tailcfg.C2NSSHUsernamesRequest) (*ta
if err != nil {
return nil, err
}
- lineread.Reader(bytes.NewReader(out), func(line []byte) error {
+ for line := range lineiter.Bytes(out) {
line = bytes.TrimSpace(line)
if len(line) == 0 || line[0] == '_' {
- return nil
+ continue
}
add(string(line))
- return nil
- })
+ }
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)
if len(line) == 0 || line[0] == '#' || line[0] == '_' {
- return nil
+ continue
}
if mem.HasSuffix(mem.B(line), mem.S("/nologin")) ||
mem.HasSuffix(mem.B(line), mem.S("/false")) {
- return nil
+ continue
}
colon := bytes.IndexByte(line, ':')
if colon != -1 {
add(string(line[:colon]))
}
- return nil
- })
+ }
}
return res, nil
}
M net/netmon/interfaces_android.go => net/netmon/interfaces_android.go +23 -28
@@ 5,7 5,6 @@ package netmon
import (
"bytes"
- "errors"
"log"
"net/netip"
"os/exec"
@@ 15,7 14,7 @@ import (
"golang.org/x/sys/unix"
"tailscale.com/net/netaddr"
"tailscale.com/syncs"
- "tailscale.com/util/lineread"
+ "tailscale.com/util/lineiter"
)
var (
@@ 34,11 33,6 @@ func init() {
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:
@@ 54,44 48,42 @@ func likelyHomeRouterIPAndroid() (ret netip.Addr, myIP netip.Addr, ok bool) {
}
lineNum := 0
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++
if lineNum == 1 {
// Skip header line.
- return nil
+ continue
}
if lineNum > maxProcNetRouteRead {
- return errStopReading
+ break
}
f = mem.AppendFields(f[:0], mem.B(line))
if len(f) < 4 {
- return nil
+ continue
}
gwHex, flagsHex := f[2], f[3]
flags, err := mem.ParseUint(flagsHex, 16, 16)
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 {
- return nil
+ continue
}
ipu32, err := mem.ParseUint(gwHex, 16, 32)
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))
if ip.IsPrivate() {
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() {
// 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
}
// 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 "
if !mem.HasPrefix(mem.B(line), mem.S(pfx)) {
- return nil
+ continue
}
line = line[len(pfx):]
sp := bytes.IndexByte(line, ' ')
if sp == -1 {
- return nil
+ continue
}
ipb := line[:sp]
if ip, err := netip.ParseAddr(string(ipb)); err == nil && ip.Is4() {
ret = ip
log.Printf("interfaces: found Android default route %v", ip)
}
- return nil
- })
+ }
cmd.Process.Kill()
cmd.Wait()
return ret, netip.Addr{}, ret.IsValid()
M net/netmon/interfaces_darwin_test.go => net/netmon/interfaces_darwin_test.go +12 -12
@@ 4,14 4,13 @@
package netmon
import (
- "errors"
"io"
"net/netip"
"os/exec"
"testing"
"go4.org/mem"
- "tailscale.com/util/lineread"
+ "tailscale.com/util/lineiter"
"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
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)
if !mem.Contains(line, mem.S("default")) {
- return nil
+ continue
}
f = mem.AppendFields(f[:0], line)
if len(f) < 4 || !f[0].EqualString("default") {
- return nil
+ continue
}
ipm, flagsm, netifm := f[1], f[2], f[3]
if !mem.Contains(flagsm, mem.S("G")) {
- return nil
+ continue
}
if mem.Contains(flagsm, mem.S("I")) {
- return nil
+ continue
}
ip, err := netip.ParseAddr(string(mem.Append(nil, ipm)))
if err == nil && ip.IsPrivate() {
ret = ip
netif = netifm.StringCopy()
// We've found what we're looking for.
- return errStopReadingNetstatTable
+ break
}
- return nil
- })
+ }
return ret, netif, ret.IsValid()
}
@@ 110,5 112,3 @@ func TestFetchRoutingTable(t *testing.T) {
}
}
}
-
-var errStopReadingNetstatTable = errors.New("found private gateway")
M net/netmon/interfaces_linux.go => net/netmon/interfaces_linux.go +15 -22
@@ 23,7 23,7 @@ import (
"go4.org/mem"
"golang.org/x/sys/unix"
"tailscale.com/net/netaddr"
- "tailscale.com/util/lineread"
+ "tailscale.com/util/lineiter"
)
func init() {
@@ 32,11 32,6 @@ func init() {
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:
@@ 52,44 47,42 @@ func likelyHomeRouterIPLinux() (ret netip.Addr, myIP netip.Addr, ok bool) {
}
lineNum := 0
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++
if lineNum == 1 {
// Skip header line.
- return nil
+ continue
}
if lineNum > maxProcNetRouteRead {
- return errStopReading
+ break
}
f = mem.AppendFields(f[:0], mem.B(line))
if len(f) < 4 {
- return nil
+ continue
}
gwHex, flagsHex := f[2], f[3]
flags, err := mem.ParseUint(flagsHex, 16, 16)
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 {
- return nil
+ continue
}
ipu32, err := mem.ParseUint(gwHex, 16, 32)
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))
if ip.IsPrivate() {
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() {
// Try to get the local IP of the interface associated with
M net/netmon/netmon_linux_test.go => net/netmon/netmon_linux_test.go +2 -0
@@ 1,6 1,8 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
+//go:build linux && !android
+
package netmon
import (
M net/tshttpproxy/tshttpproxy_synology.go => net/tshttpproxy/tshttpproxy_synology.go +8 -7
@@ 17,7 17,7 @@ import (
"sync"
"time"
- "tailscale.com/util/lineread"
+ "tailscale.com/util/lineiter"
)
// 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) {
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
line = bytes.TrimSpace(line)
if len(line) == 0 {
- return nil
+ continue
}
key, value, ok := strings.Cut(string(line), "=")
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)
- return nil
- }); err != nil {
- return nil, nil, err
}
if cfg["proxy_enabled"] != "yes" {
M ssh/tailssh/tailssh_test.go => ssh/tailssh/tailssh_test.go +5 -8
@@ 48,7 48,7 @@ import (
"tailscale.com/types/netmap"
"tailscale.com/types/ptr"
"tailscale.com/util/cibuild"
- "tailscale.com/util/lineread"
+ "tailscale.com/util/lineiter"
"tailscale.com/util/must"
"tailscale.com/version/distro"
"tailscale.com/wgengine"
@@ 1123,14 1123,11 @@ func TestSSH(t *testing.T) {
func parseEnv(out []byte) 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
}
M ssh/tailssh/user.go => ssh/tailssh/user.go +9 -9
@@ 6,7 6,6 @@
package tailssh
import (
- "io"
"os"
"os/exec"
"os/user"
@@ 18,7 17,7 @@ import (
"go4.org/mem"
"tailscale.com/envknob"
"tailscale.com/hostinfo"
- "tailscale.com/util/lineread"
+ "tailscale.com/util/lineiter"
"tailscale.com/util/osuser"
"tailscale.com/version/distro"
)
@@ 110,15 109,16 @@ func defaultPathForUser(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 != "" {
- path = v
- return io.EOF // stop iteration
+ return v
}
- return nil
- })
- return path
+ }
+ return ""
}
func pathFromPAMEnvLine(line []byte, u *user.User) (path string) {
A types/result/result.go => types/result/result.go +49 -0
@@ 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
+}
A util/lineiter/lineiter.go => util/lineiter/lineiter.go +72 -0
@@ 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))
+ }
+ }
+}
A util/lineiter/lineiter_test.go => util/lineiter/lineiter_test.go +32 -0
@@ 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)
+ }
+}
M util/pidowner/pidowner_linux.go => util/pidowner/pidowner_linux.go +10 -10
@@ 8,26 8,26 @@ import (
"os"
"strings"
- "tailscale.com/util/lineread"
+ "tailscale.com/util/lineiter"
)
func ownerOfPID(pid int) (userID string, err error) {
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:" {
- return nil
+ continue
}
f := strings.Fields(string(line))
if len(f) >= 2 {
userID = f[1] // real userid
}
- return nil
- })
- if os.IsNotExist(err) {
- return "", ErrProcessNotFound
- }
- if err != nil {
- return
}
if userID == "" {
return "", fmt.Errorf("missing Uid line in %s", file)
M version/distro/distro.go => version/distro/distro.go +10 -10
@@ 6,13 6,12 @@ package distro
import (
"bytes"
- "io"
"os"
"runtime"
"strconv"
"tailscale.com/types/lazy"
- "tailscale.com/util/lineread"
+ "tailscale.com/util/lineiter"
)
type Distro string
@@ 132,18 131,19 @@ func DSMVersion() int {
return v
}
// 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)
if string(line) == `majorversion="7"` {
- v = 7
- return io.EOF
+ return 7
}
if string(line) == `majorversion="6"` {
- v = 6
- return io.EOF
+ return 6
}
- return nil
- })
- return v
+ }
+ return 0
})
}