tailscale/version/cmdname.go
Brad Fitzpatrick 006d7e180e version: use debug.ReadBuildInfo in CmdName on non-Windows
CmdName was re-opening the running executable and scanning it in
64KiB chunks for the Go modinfo markers on every call. The same
modinfo is already parsed at startup and exposed via
runtime/debug.ReadBuildInfo, so prefer that on non-Windows. Windows
still takes the scanning path because its GUI-binary override keys
off the on-disk executable name.

benchstat of BenchmarkCmdName (Linux, before vs after):

    goos: linux
    goarch: amd64
    pkg: tailscale.com/version
    cpu: Intel(R) Xeon(R) 6975P-C
               │  /tmp/old.txt  │            /tmp/new.txt             │
               │     sec/op     │   sec/op     vs base                │
    CmdName-16   556045.5n ± 1%   825.6n ± 1%  -99.85% (p=0.000 n=10)

               │ /tmp/old.txt  │             /tmp/new.txt             │
               │     B/op      │     B/op      vs base                │
    CmdName-16   64.587Ki ± 0%   1.156Ki ± 0%  -98.21% (p=0.000 n=10)

               │ /tmp/old.txt │            /tmp/new.txt            │
               │  allocs/op   │ allocs/op   vs base                │
    CmdName-16     8.000 ± 0%   7.000 ± 0%  -12.50% (p=0.000 n=10)

Fixes #19486

Change-Id: I925c5e28b64815a602459beb6c8dab8779339a6c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-04-24 09:48:11 -07:00

150 lines
4.0 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ios
package version
import (
"bytes"
"encoding/hex"
"errors"
"io"
"os"
"path"
"runtime"
"runtime/debug"
"strings"
)
// 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 {
// On non-Windows, the modinfo embedded in the running binary is
// authoritative and avoids re-reading the executable from disk.
// Windows needs the executable-name-based GUI override in cmdName,
// so it still takes the slower path.
if runtime.GOOS != "windows" {
if info, ok := debug.ReadBuildInfo(); ok && info.Path != "" {
return path.Base(info.Path)
}
}
e, err := os.Executable()
if err != nil {
return "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 {
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
}
}
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 == "" {
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")
)