diff --git a/version/cmdname.go b/version/cmdname.go index 5a0b84875..137979f82 100644 --- a/version/cmdname.go +++ b/version/cmdname.go @@ -6,134 +6,51 @@ package version import ( - "bytes" - "encoding/hex" - "errors" - "io" "os" "path" "runtime" + "runtime/debug" "strings" + "sync" ) // CmdName returns either the base name of the current binary // using os.Executable. If os.Executable fails (it shouldn't), then // "cmd" is returned. -func CmdName() string { - e, err := os.Executable() - if err != nil { - return "cmd" +// +// The result is computed once per process and cached. It is recovered +// from the Go module info embedded in the running binary via +// [runtime/debug.ReadBuildInfo], which reads an already-resident +// string maintained by the runtime; no filesystem I/O is performed. +// This is materially cheaper than inferring the command name from +// the on-disk executable, which was previously done by scanning the +// entire binary for magic bytes on every call. CmdName is called at +// least twice during tailscaled startup on Windows (by logpolicy). +func CmdName() string { return cmdNameCached() } + +var cmdNameCached = sync.OnceValue(func() string { + // fallbackName is derived from os.Executable and used if we cannot + // recover a package path from the binary's embedded build info. + var fallbackName string + if e, err := os.Executable(); err == nil { + fallbackName = prepExeNameForCmp(e, runtime.GOARCH) + } else { + fallbackName = "cmd" } - return cmdName(e) -} -func cmdName(exe string) string { - // fallbackName, the lowercase basename of the executable, is what we return if - // we can't find the Go module metadata embedded in the file. - fallbackName := prepExeNameForCmp(exe, runtime.GOARCH) - - var ret string - info, err := findModuleInfo(exe) - if err != nil { + bi, ok := debug.ReadBuildInfo() + if !ok || bi.Path == "" { return fallbackName } - // v is like: - // "path\ttailscale.com/cmd/tailscale\nmod\ttailscale.com\t(devel)\t\ndep\tgithub.com/apenwarr/fixconsole\tv0.0.0-20191012055117-5a9f6489cc29\th1:muXWUcay7DDy1/hEQWrYlBy+g0EuwT70sBHg65SeUc4=\ndep\tgithub.... - for line := range strings.SplitSeq(info, "\n") { - if goPkg, ok := strings.CutPrefix(line, "path\t"); ok { // like "tailscale.com/cmd/tailscale" - ret = path.Base(goPkg) // goPkg is always forward slashes; use path, not filepath - break - } - } + // bi.Path is the main package import path, e.g. + // "tailscale.com/cmd/tailscaled". Go import paths are always + // forward-slash separated, so use path.Base, not filepath.Base. + ret := path.Base(bi.Path) if runtime.GOOS == "windows" && strings.HasPrefix(ret, "gui") && checkPreppedExeNameForGUI(fallbackName) { - // The GUI binary for internal build system packaging reasons - // has a path of "tailscale.io/win/gui". - // Ignore that name and use fallbackName instead. - return fallbackName - } - if ret == "" { + // The GUI binary, for internal build-system packaging reasons, + // has a path of "tailscale.io/win/gui". Ignore that name and + // use fallbackName instead. return fallbackName } return ret -} - -// findModuleInfo returns the Go module info from the executable file. -func findModuleInfo(file string) (s string, err error) { - f, err := os.Open(file) - if err != nil { - return "", err - } - defer f.Close() - // Scan through f until we find infoStart. - buf := make([]byte, 65536) - start, err := findOffset(f, buf, infoStart) - if err != nil { - return "", err - } - start += int64(len(infoStart)) - // Seek to the end of infoStart and scan for infoEnd. - _, err = f.Seek(start, io.SeekStart) - if err != nil { - return "", err - } - end, err := findOffset(f, buf, infoEnd) - if err != nil { - return "", err - } - length := end - start - // As of Aug 2021, tailscaled's mod info was about 2k. - if length > int64(len(buf)) { - return "", errors.New("mod info too large") - } - // We have located modinfo. Read it into buf. - buf = buf[:length] - _, err = f.Seek(start, io.SeekStart) - if err != nil { - return "", err - } - _, err = io.ReadFull(f, buf) - if err != nil { - return "", err - } - return string(buf), nil -} - -// findOffset finds the absolute offset of needle in f, -// starting at f's current read position, -// using temporary buffer buf. -func findOffset(f *os.File, buf, needle []byte) (int64, error) { - for { - // Fill buf and look within it. - n, err := f.Read(buf) - if err != nil { - return -1, err - } - i := bytes.Index(buf[:n], needle) - if i < 0 { - // Not found. Rewind a little bit in case we happened to end halfway through needle. - rewind, err := f.Seek(int64(-len(needle)), io.SeekCurrent) - if err != nil { - return -1, err - } - // If we're at EOF and rewound exactly len(needle) bytes, return io.EOF. - _, err = f.ReadAt(buf[:1], rewind+int64(len(needle))) - if err == io.EOF { - return -1, err - } - continue - } - // Found! Figure out exactly where. - cur, err := f.Seek(0, io.SeekCurrent) - if err != nil { - return -1, err - } - return cur - int64(n) + int64(i), nil - } -} - -// These constants are taken from rsc.io/goversion. - -var ( - infoStart, _ = hex.DecodeString("3077af0c9274080241e1c107e6d618e6") - infoEnd, _ = hex.DecodeString("f932433186182072008242104116d8f2") -) +}) diff --git a/version/cmdname_test.go b/version/cmdname_test.go new file mode 100644 index 000000000..94f4e984d --- /dev/null +++ b/version/cmdname_test.go @@ -0,0 +1,54 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +//go:build !ios + +package version_test + +import ( + "testing" + + "tailscale.com/tstest" + "tailscale.com/version" +) + +// TestCmdNameFromBuildInfo asserts that CmdName recovers its result from the +// running binary's embedded Go module info (via runtime/debug.ReadBuildInfo) +// rather than returning the os.Executable-based fallback. When this test is +// run under "go test tailscale.com/version", the test binary's embedded +// build-info Path is "tailscale.com/version.test", so CmdName should return +// "version.test". The on-disk basename of the test binary (something like +// "version.test" in a go-build temp dir with random suffixes) is also +// typically "version.test", but the import-path derivation is what we care +// about: it is the only route by which a binary installed under an arbitrary +// name (e.g. "tailscaled-linux-amd64") still reports itself as "tailscaled". +func TestCmdNameFromBuildInfo(t *testing.T) { + if got, want := version.CmdName(), "version.test"; got != want { + t.Errorf("CmdName() = %q, want %q", got, want) + } +} + +// BenchmarkCmdName measures the cost of the public, memoized CmdName. +// After a one-time warmup (which itself does no filesystem I/O, just an +// in-memory string lookup), this should be a trivial atomic load with zero +// allocations. +func BenchmarkCmdName(b *testing.B) { + _ = version.CmdName() // prime + b.ReportAllocs() + b.ResetTimer() + for range b.N { + _ = version.CmdName() + } +} + +// TestCmdNameNoAllocs asserts that the public CmdName, once primed, performs +// no allocations. This guards against regressions that reintroduce per-call +// binary parsing. +func TestCmdNameNoAllocs(t *testing.T) { + _ = version.CmdName() // prime + if err := tstest.MinAllocsPerRun(t, 0, func() { + _ = version.CmdName() + }); err != nil { + t.Error(err) + } +} diff --git a/version/export_test.go b/version/export_test.go index ec43ad332..8500bf261 100644 --- a/version/export_test.go +++ b/version/export_test.go @@ -4,9 +4,7 @@ package version var ( - ExportParse = parse - ExportFindModuleInfo = findModuleInfo - ExportCmdName = cmdName + ExportParse = parse ) type ( diff --git a/version/modinfo_test.go b/version/modinfo_test.go deleted file mode 100644 index ef75ce077..000000000 --- a/version/modinfo_test.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Tailscale Inc & contributors -// SPDX-License-Identifier: BSD-3-Clause - -package version_test - -import ( - "flag" - "os/exec" - "path/filepath" - "strings" - "testing" - - "tailscale.com/version" -) - -var ( - findModuleInfo = version.ExportFindModuleInfo - cmdName = version.ExportCmdName -) - -func TestFindModuleInfo(t *testing.T) { - dir := t.TempDir() - name := filepath.Join(dir, "tailscaled-version-test") - out, err := exec.Command("go", "build", "-o", name, "tailscale.com/cmd/tailscaled").CombinedOutput() - if err != nil { - t.Fatalf("failed to build tailscaled: %v\n%s", err, out) - } - modinfo, err := findModuleInfo(name) - if err != nil { - t.Fatal(err) - } - prefix := "path\ttailscale.com/cmd/tailscaled\nmod\ttailscale.com" - if !strings.HasPrefix(modinfo, prefix) { - t.Errorf("unexpected modinfo contents %q", modinfo) - } -} - -var findModuleInfoName = flag.String("module-info-file", "", "if non-empty, test findModuleInfo against this filename") - -func TestFindModuleInfoManual(t *testing.T) { - exe := *findModuleInfoName - if exe == "" { - t.Skip("skipping without --module-info-file filename") - } - cmd := cmdName(exe) - mod, err := findModuleInfo(exe) - if err != nil { - t.Fatal(err) - } - t.Logf("Got %q from: %s", cmd, mod) -}