cmdname.go 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. //go:build !ios
  4. package version
  5. import (
  6. "bytes"
  7. "encoding/hex"
  8. "errors"
  9. "io"
  10. "os"
  11. "path"
  12. "runtime"
  13. "strings"
  14. )
  15. // CmdName returns either the base name of the current binary
  16. // using os.Executable. If os.Executable fails (it shouldn't), then
  17. // "cmd" is returned.
  18. func CmdName() string {
  19. e, err := os.Executable()
  20. if err != nil {
  21. return "cmd"
  22. }
  23. return cmdName(e)
  24. }
  25. func cmdName(exe string) string {
  26. // fallbackName, the lowercase basename of the executable, is what we return if
  27. // we can't find the Go module metadata embedded in the file.
  28. fallbackName := prepExeNameForCmp(exe, runtime.GOARCH)
  29. var ret string
  30. info, err := findModuleInfo(exe)
  31. if err != nil {
  32. return fallbackName
  33. }
  34. // v is like:
  35. // "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....
  36. for _, line := range strings.Split(info, "\n") {
  37. if goPkg, ok := strings.CutPrefix(line, "path\t"); ok { // like "tailscale.com/cmd/tailscale"
  38. ret = path.Base(goPkg) // goPkg is always forward slashes; use path, not filepath
  39. break
  40. }
  41. }
  42. if runtime.GOOS == "windows" && strings.HasPrefix(ret, "gui") && checkPreppedExeNameForGUI(fallbackName) {
  43. // The GUI binary for internal build system packaging reasons
  44. // has a path of "tailscale.io/win/gui".
  45. // Ignore that name and use fallbackName instead.
  46. return fallbackName
  47. }
  48. if ret == "" {
  49. return fallbackName
  50. }
  51. return ret
  52. }
  53. // findModuleInfo returns the Go module info from the executable file.
  54. func findModuleInfo(file string) (s string, err error) {
  55. f, err := os.Open(file)
  56. if err != nil {
  57. return "", err
  58. }
  59. defer f.Close()
  60. // Scan through f until we find infoStart.
  61. buf := make([]byte, 65536)
  62. start, err := findOffset(f, buf, infoStart)
  63. if err != nil {
  64. return "", err
  65. }
  66. start += int64(len(infoStart))
  67. // Seek to the end of infoStart and scan for infoEnd.
  68. _, err = f.Seek(start, io.SeekStart)
  69. if err != nil {
  70. return "", err
  71. }
  72. end, err := findOffset(f, buf, infoEnd)
  73. if err != nil {
  74. return "", err
  75. }
  76. length := end - start
  77. // As of Aug 2021, tailscaled's mod info was about 2k.
  78. if length > int64(len(buf)) {
  79. return "", errors.New("mod info too large")
  80. }
  81. // We have located modinfo. Read it into buf.
  82. buf = buf[:length]
  83. _, err = f.Seek(start, io.SeekStart)
  84. if err != nil {
  85. return "", err
  86. }
  87. _, err = io.ReadFull(f, buf)
  88. if err != nil {
  89. return "", err
  90. }
  91. return string(buf), nil
  92. }
  93. // findOffset finds the absolute offset of needle in f,
  94. // starting at f's current read position,
  95. // using temporary buffer buf.
  96. func findOffset(f *os.File, buf, needle []byte) (int64, error) {
  97. for {
  98. // Fill buf and look within it.
  99. n, err := f.Read(buf)
  100. if err != nil {
  101. return -1, err
  102. }
  103. i := bytes.Index(buf[:n], needle)
  104. if i < 0 {
  105. // Not found. Rewind a little bit in case we happened to end halfway through needle.
  106. rewind, err := f.Seek(int64(-len(needle)), io.SeekCurrent)
  107. if err != nil {
  108. return -1, err
  109. }
  110. // If we're at EOF and rewound exactly len(needle) bytes, return io.EOF.
  111. _, err = f.ReadAt(buf[:1], rewind+int64(len(needle)))
  112. if err == io.EOF {
  113. return -1, err
  114. }
  115. continue
  116. }
  117. // Found! Figure out exactly where.
  118. cur, err := f.Seek(0, io.SeekCurrent)
  119. if err != nil {
  120. return -1, err
  121. }
  122. return cur - int64(n) + int64(i), nil
  123. }
  124. }
  125. // These constants are taken from rsc.io/goversion.
  126. var (
  127. infoStart, _ = hex.DecodeString("3077af0c9274080241e1c107e6d618e6")
  128. infoEnd, _ = hex.DecodeString("f932433186182072008242104116d8f2")
  129. )