mirror of
https://github.com/tailscale/tailscale.git
synced 2026-05-06 04:36:15 +02:00
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>
150 lines
4.0 KiB
Go
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")
|
|
)
|