mirror of
https://github.com/tailscale/tailscale.git
synced 2026-06-23 23:41:41 -04:00
ipn: add fmt.Stringer support to NotifyWatchOpt (#20072)
This patch adds support for the fmt.Stringer interface to the
ipn.NotifyWatchOpt enum. This is useful when debugging these bitmasks.
For example:
fmt.Printf("%s", ipn.NotifyPeerChanges | ipn.NotifyNoNetMap)
// Output: (ipn.NotifyPeerChanges | ipn.NotifyNoNetMap)
Fixes #20066
Signed-off-by: Simon Law <sfllaw@tailscale.com>
This commit is contained in:
@@ -1365,8 +1365,12 @@ func (lc *Client) StreamDebugCapture(ctx context.Context) (io.ReadCloser, error)
|
||||
//
|
||||
// A default set of ipn.Notify messages are returned but the set can be modified by mask.
|
||||
func (lc *Client) WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (*IPNBusWatcher, error) {
|
||||
m, err := mask.MarshalText()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, "GET",
|
||||
"http://"+apitype.LocalAPIHost+"/localapi/v0/watch-ipn-bus?mask="+fmt.Sprint(mask),
|
||||
"http://"+apitype.LocalAPIHost+"/localapi/v0/watch-ipn-bus?mask="+string(m),
|
||||
nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -180,6 +181,81 @@ type EngineStatus struct {
|
||||
NotifyPeerWireGuardState NotifyWatchOpt = 1 << 18
|
||||
)
|
||||
|
||||
// String implements the [fmt.Stringer] interface.
|
||||
// Returns the string representation of all the bits joined by the bitwise-or "|" operator.
|
||||
func (o NotifyWatchOpt) String() string {
|
||||
if o == NotifyWatchOpt(0) {
|
||||
return fmt.Sprintf("%T(%#x)", o, uint64(o))
|
||||
}
|
||||
|
||||
pkg, _, found := strings.Cut(fmt.Sprintf("%T", o), ".")
|
||||
|
||||
var bits []string
|
||||
var mask NotifyWatchOpt
|
||||
try := func(bit NotifyWatchOpt, s string) {
|
||||
if o&bit == 0 {
|
||||
return
|
||||
}
|
||||
if found {
|
||||
bits = append(bits, pkg+"."+s)
|
||||
} else {
|
||||
bits = append(bits, s)
|
||||
}
|
||||
mask |= bit
|
||||
}
|
||||
try(NotifyWatchEngineUpdates, "NotifyWatchEngineUpdates")
|
||||
try(NotifyInitialState, "NotifyInitialState")
|
||||
try(NotifyInitialPrefs, "NotifyInitialPrefs")
|
||||
try(NotifyInitialNetMap, "NotifyInitialNetMap")
|
||||
try(NotifyNoPrivateKeys, "NotifyNoPrivateKeys")
|
||||
try(NotifyInitialDriveShares, "NotifyInitialDriveShares")
|
||||
try(NotifyInitialOutgoingFiles, "NotifyInitialOutgoingFiles")
|
||||
try(NotifyInitialHealthState, "NotifyInitialHealthState")
|
||||
try(NotifyRateLimit, "NotifyRateLimit")
|
||||
try(NotifyHealthActions, "NotifyHealthActions")
|
||||
try(NotifyInitialSuggestedExitNode, "NotifyInitialSuggestedExitNode")
|
||||
try(NotifyInitialClientVersion, "NotifyInitialClientVersion")
|
||||
try(NotifyPeerChanges, "NotifyPeerChanges")
|
||||
try(NotifyNoNetMap, "NotifyNoNetMap")
|
||||
try(NotifyInitialStatus, "NotifyInitialStatus")
|
||||
try(NotifyPeerPatches, "NotifyPeerPatches")
|
||||
try(NotifyInProcessNoDisconnect, "NotifyInProcessNoDisconnect")
|
||||
try(NotifyPeerWireGuardState, "NotifyPeerWireGuardState")
|
||||
|
||||
if mask != o {
|
||||
bits = append(bits, fmt.Sprintf("%T(%#x)", o, uint64(o^mask))) // unknown
|
||||
}
|
||||
|
||||
if len(bits) == 1 {
|
||||
return bits[0]
|
||||
}
|
||||
// Multiple values, so we need to wrap with parentheses.
|
||||
return "(" + strings.Join(bits, " | ") + ")"
|
||||
}
|
||||
|
||||
// AppendText implements the [encoding.TextAppender] interface
|
||||
// by encoding its textual representation.
|
||||
func (o NotifyWatchOpt) AppendText(b []byte) ([]byte, error) {
|
||||
return strconv.AppendUint(b, uint64(o), 10), nil
|
||||
}
|
||||
|
||||
// MarshalText implements the [encoding.TextMarshaler] interface
|
||||
// by encoding its textual representation.
|
||||
func (o NotifyWatchOpt) MarshalText() (text []byte, err error) {
|
||||
return o.AppendText(nil)
|
||||
}
|
||||
|
||||
// UnmarshalText implements the [encoding.TextUnmarshaler] interface
|
||||
// by decoding its textual representation.
|
||||
func (o *NotifyWatchOpt) UnmarshalText(text []byte) error {
|
||||
v, err := strconv.ParseUint(string(text), 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*o = NotifyWatchOpt(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NotifyRateLimitIncompatibleBits is the set of new-style IPN bus
|
||||
// subscription bits that cannot be combined with [NotifyRateLimit].
|
||||
//
|
||||
|
||||
@@ -5,10 +5,19 @@
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"go/types"
|
||||
"maps"
|
||||
"math/bits"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/go/packages"
|
||||
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/types/empty"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
func TestNotifyString(t *testing.T) {
|
||||
@@ -126,3 +135,133 @@ func TestValidateNotifyWatchOpt(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotifyWatchOptString(t *testing.T) {
|
||||
consts := findNotifyWatchOptConstants(t)
|
||||
t.Logf("consts = %#v", consts)
|
||||
|
||||
t.Run("zero", func(t *testing.T) {
|
||||
var zero NotifyWatchOpt
|
||||
want := "ipn.NotifyWatchOpt(0x0)"
|
||||
if got := zero.String(); got != want {
|
||||
t.Errorf("NotifyWatchOpt(%#v).String() = %q, want %q", zero, got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unknown", func(t *testing.T) {
|
||||
msb := NotifyWatchOpt(1 << 63)
|
||||
want := "ipn.NotifyWatchOpt(0x8000000000000000)"
|
||||
if got := msb.String(); got != want {
|
||||
t.Errorf("NotifyWatchOpt(%#v).String() = %q, want %q", msb, got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("simple", func(t *testing.T) {
|
||||
for _, c := range slices.Sorted(maps.Keys(consts)) {
|
||||
if bits.OnesCount64(uint64(c)) > 1 {
|
||||
continue // multiple bits comes later
|
||||
}
|
||||
want := "ipn." + consts[c]
|
||||
if got := c.String(); got != want {
|
||||
t.Errorf("NotifyWatchOpt(%#v).String() = %q, want %q", c, got, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("composite", func(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
value NotifyWatchOpt
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "single",
|
||||
value: NotifyWatchEngineUpdates,
|
||||
want: "ipn.NotifyWatchEngineUpdates",
|
||||
},
|
||||
{
|
||||
name: "double",
|
||||
value: NotifyWatchEngineUpdates | NotifyInitialState,
|
||||
want: "(ipn.NotifyWatchEngineUpdates | ipn.NotifyInitialState)",
|
||||
},
|
||||
{
|
||||
name: "triple",
|
||||
value: NotifyWatchEngineUpdates | NotifyInitialState | NotifyInitialPrefs,
|
||||
want: "(ipn.NotifyWatchEngineUpdates | ipn.NotifyInitialState | ipn.NotifyInitialPrefs)",
|
||||
},
|
||||
{
|
||||
name: "unknown",
|
||||
value: NotifyWatchEngineUpdates | NotifyWatchOpt(1<<63),
|
||||
want: "(ipn.NotifyWatchEngineUpdates | ipn.NotifyWatchOpt(0x8000000000000000))",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := tc.value.String(); got != tc.want {
|
||||
t.Errorf("NotifyWatchOpt(%#v).String() = %q, want %q", tc.value, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Check that every named NotifyWatchOpt value is mapped inside [NotifyWatchOpt.String].
|
||||
t.Run("all", func(t *testing.T) {
|
||||
var all NotifyWatchOpt
|
||||
var names []string // names are sorted and only contain simple consts
|
||||
for _, c := range slices.Sorted(maps.Keys(consts)) {
|
||||
all |= c
|
||||
if bits.OnesCount64(uint64(c)) == 1 {
|
||||
names = append(names, "ipn."+consts[c])
|
||||
}
|
||||
}
|
||||
want := "(" + strings.Join(names, " | ") + ")"
|
||||
if got := all.String(); got != want {
|
||||
t.Errorf("all.String() = %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func findNotifyWatchOptConstants(t *testing.T) map[NotifyWatchOpt]string {
|
||||
t.Helper()
|
||||
|
||||
// Load the current package.
|
||||
cfg := &packages.Config{
|
||||
Mode: packages.NeedTypes,
|
||||
}
|
||||
pkgs, err := packages.Load(cfg, ".")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load packages: %v", err)
|
||||
}
|
||||
|
||||
// Find all the [NotifyWatchOpt] constants that represent this enum.
|
||||
var found map[NotifyWatchOpt]string
|
||||
for _, pkg := range pkgs {
|
||||
if len(pkg.Errors) > 0 {
|
||||
t.Fatalf("package %s has errors: %v", pkg.Name, pkg.Errors)
|
||||
}
|
||||
|
||||
wantType := pkg.Types.Path() + ".NotifyWatchOpt"
|
||||
scope := pkg.Types.Scope()
|
||||
for _, name := range scope.Names() {
|
||||
obj := scope.Lookup(name)
|
||||
if obj == nil || obj.Type().String() != wantType {
|
||||
continue
|
||||
}
|
||||
c, ok := obj.(*types.Const)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
s := c.Val().ExactString()
|
||||
val, err := strconv.ParseUint(s, 10, 64)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot parse %q: %v", s, err)
|
||||
}
|
||||
mak.Set(&found, NotifyWatchOpt(val), name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(found) == 0 {
|
||||
t.Fatal("could not find NotifyWatchOpt constants")
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
@@ -896,12 +896,10 @@ func (h *Handler) serveWatchIPNBus(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var mask ipn.NotifyWatchOpt
|
||||
if s := r.FormValue("mask"); s != "" {
|
||||
v, err := strconv.ParseUint(s, 10, 64)
|
||||
if err != nil {
|
||||
if err := mask.UnmarshalText([]byte(s)); err != nil {
|
||||
http.Error(w, "bad mask", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
mask = ipn.NotifyWatchOpt(v)
|
||||
}
|
||||
if mask&ipn.NotifyInProcessNoDisconnect != 0 {
|
||||
http.Error(w, "NotifyInProcessNoDisconnect is only valid for in-process IPN bus subscribers", http.StatusBadRequest)
|
||||
|
||||
Reference in New Issue
Block a user