summaryrefslogtreecommitdiffhomepage
path: root/version/cmdname.go
blob: 8e6adb047c8ccc71393d3b2a52ba3d69d2401961 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
// 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")
)