summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--version/cmdname.go145
-rw-r--r--version/cmdname_test.go54
-rw-r--r--version/export_test.go4
-rw-r--r--version/modinfo_test.go51
4 files changed, 86 insertions, 168 deletions
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"
- }
- return cmdName(e)
-}
+//
+// 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() }
-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 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"
+ }
- 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)
-}