github.com/mutagen-io/mutagen@v0.18.0-rc1/pkg/agent/probe.go (about)

     1  package agent
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"strings"
     7  	"unicode/utf8"
     8  
     9  	"github.com/mutagen-io/mutagen/pkg/environment"
    10  	"github.com/mutagen-io/mutagen/pkg/prompting"
    11  )
    12  
    13  // unameSToGOOS maps uname -s output values to their corresponding GOOS values.
    14  // Although some Windows environments (Cygwin, MSYS, and MinGW) support uname,
    15  // their values are handled by unameSIsWindowsPosix because they are so varied
    16  // (their value depends on the POSIX environment and its version, the system
    17  // architecture, and the NT kernel version).
    18  var unameSToGOOS = map[string]string{
    19  	"AIX":       "aix",
    20  	"Darwin":    "darwin",
    21  	"DragonFly": "dragonfly",
    22  	"FreeBSD":   "freebsd",
    23  	"Linux":     "linux",
    24  	"NetBSD":    "netbsd",
    25  	"OpenBSD":   "openbsd",
    26  	"SunOS":     "solaris",
    27  	// TODO: Add more obscure uname -s values as necessary, e.g.
    28  	// debian/kFreeBSD, which returns "GNU/kFreeBSD".
    29  }
    30  
    31  // unameSIsWindowsPosix determines whether or not a uname -s output value
    32  // represents a Windows POSIX environment.
    33  func unameSIsWindowsPosix(value string) bool {
    34  	return strings.HasPrefix(value, "CYGWIN") ||
    35  		strings.HasPrefix(value, "MINGW") ||
    36  		strings.HasPrefix(value, "MSYS")
    37  }
    38  
    39  // unameMToGOARCH maps uname -m output values to their corresponding GOARCH
    40  // values.
    41  var unameMToGOARCH = map[string]string{
    42  	"i386":     "386",
    43  	"i486":     "386",
    44  	"i586":     "386",
    45  	"i686":     "386",
    46  	"x86_64":   "amd64",
    47  	"amd64":    "amd64",
    48  	"armv5l":   "arm",
    49  	"armv6l":   "arm",
    50  	"armv7l":   "arm",
    51  	"armv8l":   "arm64",
    52  	"aarch64":  "arm64",
    53  	"arm64":    "arm64",
    54  	"mips":     "mips",
    55  	"mipsel":   "mipsle",
    56  	"mips64":   "mips64",
    57  	"mips64el": "mips64le",
    58  	"ppc64":    "ppc64",
    59  	"ppc64le":  "ppc64le",
    60  	"riscv64":  "riscv64",
    61  	"s390x":    "s390x",
    62  	// TODO: Add any more obscure uname -m variations that we might encounter.
    63  }
    64  
    65  // osEnvToGOOS maps the value of the "OS" environment variable on Windows to the
    66  // corresponding GOOS. There's only one supported value, but we keep things this
    67  // way for symmetry and extensibility.
    68  var osEnvToGOOS = map[string]string{
    69  	"Windows_NT": "windows",
    70  }
    71  
    72  // processorArchitectureEnvToGOARCH maps the value of the
    73  // "PROCESSOR_ARCHITECTURE" environment variable on Windows to the corresponding
    74  // GOARCH.
    75  var processorArchitectureEnvToGOARCH = map[string]string{
    76  	"x86":   "386",
    77  	"AMD64": "amd64",
    78  	"ARM":   "arm",
    79  	"ARM64": "arm64",
    80  	// TODO: Add IA64 (that's the key) if Go ever supports Itanium, though
    81  	// they've pretty much stated that this will never happen:
    82  	// https://groups.google.com/forum/#!topic/golang-nuts/RgGF1Dudym4
    83  }
    84  
    85  // probePOSIX performs platform probing over an agent transport, working under
    86  // the assumption that the remote system is a POSIX system.
    87  func probePOSIX(transport Transport) (string, string, error) {
    88  	// Try to invoke uname and print kernel and machine name.
    89  	unameSMBytes, err := output(transport, "uname -s -m")
    90  	if err != nil {
    91  		return "", "", fmt.Errorf("unable to invoke uname: %w", err)
    92  	} else if !utf8.Valid(unameSMBytes) {
    93  		return "", "", errors.New("remote output is not UTF-8 encoded")
    94  	}
    95  
    96  	// Parse uname output.
    97  	unameSM := strings.Split(strings.TrimSpace(string(unameSMBytes)), " ")
    98  	if len(unameSM) != 2 {
    99  		return "", "", errors.New("invalid uname output")
   100  	}
   101  	unameS := unameSM[0]
   102  	unameM := unameSM[1]
   103  
   104  	// Translate GOOS. Windows POSIX systems typically include their NT version
   105  	// number in their uname -s output, so we have to handle those specially.
   106  	var goos string
   107  	var ok bool
   108  	if unameSIsWindowsPosix(unameS) {
   109  		goos = "windows"
   110  	} else if goos, ok = unameSToGOOS[unameS]; !ok {
   111  		return "", "", errors.New("unknown platform")
   112  	}
   113  
   114  	// Translate GOARCH. On AIX systems, uname -m returns the machine's serial
   115  	// number, but modern AIX only runs on 64-bit PowerPC anyway (and that's
   116  	// all we support), so we set the architecture directly.
   117  	var goarch string
   118  	if goos == "aix" {
   119  		goarch = "ppc64"
   120  	} else if goarch, ok = unameMToGOARCH[unameM]; !ok {
   121  		return "", "", errors.New("unknown architecture")
   122  	}
   123  
   124  	// Success.
   125  	return goos, goarch, nil
   126  }
   127  
   128  // probeWindows performs platform probing over an agent transport, working under
   129  // the assumption that the remote system is a Windows system.
   130  func probeWindows(transport Transport) (string, string, error) {
   131  	// Attempt to dump the remote environment.
   132  	outputBytes, err := output(transport, "cmd.exe /c set")
   133  	if err != nil {
   134  		return "", "", fmt.Errorf("unable to invoke remote environment printing: %w", err)
   135  	} else if !utf8.Valid(outputBytes) {
   136  		return "", "", errors.New("remote output is not UTF-8 encoded")
   137  	}
   138  
   139  	// Parse the output block into a series of KEY=value specifications.
   140  	environment := environment.ParseBlock(string(outputBytes))
   141  
   142  	// Extract the OS and PROCESSOR_ARCHITECTURE environment variables.
   143  	var os, processorArchitecture string
   144  	for _, e := range environment {
   145  		if strings.HasPrefix(e, "OS=") {
   146  			os = e[3:]
   147  		} else if strings.HasPrefix(e, "PROCESSOR_ARCHITECTURE=") {
   148  			processorArchitecture = e[23:]
   149  		}
   150  	}
   151  
   152  	// Translate to GOOS.
   153  	goos, ok := osEnvToGOOS[os]
   154  	if !ok {
   155  		return "", "", errors.New("unknown platform")
   156  	}
   157  
   158  	// Translate to GOARCH.
   159  	goarch, ok := processorArchitectureEnvToGOARCH[processorArchitecture]
   160  	if !ok {
   161  		return "", "", errors.New("unknown architecture")
   162  	}
   163  
   164  	// Success.
   165  	return goos, goarch, nil
   166  }
   167  
   168  // probe attempts to identify the properties of the target platform (namely
   169  // GOOS, GOARCH, and whether or not it's a POSIX environment (which it might be
   170  // even on Windows)) using the specified transport.
   171  func probe(transport Transport, prompter string) (string, string, bool, error) {
   172  	// Attempt to probe for a POSIX platform. This might apply to certain
   173  	// Windows environments as well.
   174  	if err := prompting.Message(prompter, "Probing endpoint (POSIX)..."); err != nil {
   175  		return "", "", false, fmt.Errorf("unable to message prompter: %w", err)
   176  	}
   177  	if goos, goarch, err := probePOSIX(transport); err == nil {
   178  		return goos, goarch, true, nil
   179  	}
   180  
   181  	// If that fails, attempt a Windows fallback.
   182  	if err := prompting.Message(prompter, "Probing endpoint (Windows)..."); err != nil {
   183  		return "", "", false, fmt.Errorf("unable to message prompter: %w", err)
   184  	}
   185  	if goos, goarch, err := probeWindows(transport); err == nil {
   186  		return goos, goarch, false, nil
   187  	}
   188  
   189  	// Failure.
   190  	return "", "", false, errors.New("exhausted probing methods")
   191  }