github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/pkg/sysinfo/sysinfo_windows.go (about)

     1  package sysinfo
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"os/exec"
     8  	"path/filepath"
     9  	"regexp"
    10  	"strconv"
    11  
    12  	"golang.org/x/sys/windows"
    13  	"golang.org/x/sys/windows/registry"
    14  
    15  	"github.com/patrickmn/go-cache"
    16  )
    17  
    18  // OS returns the system's OS
    19  func OS() OsInfo {
    20  	return Windows
    21  }
    22  
    23  // From https://msdn.microsoft.com/en-us/library/windows/desktop/ms724832%28v=vs.85%29.aspx
    24  // Note: cannot differentiate between some versions, hence the '/'. Also, unless
    25  // the program using this package is "manifested" (see above link), Windows will
    26  // not report higher than 6.2 (Windows 8 / Windows Server 2012).
    27  var versions = map[int]map[int]string{
    28  	5: {
    29  		0: "Windows 2000",
    30  		1: "Windows XP",
    31  		2: "Windows XP / Windows Server 2003",
    32  	},
    33  	6: {
    34  		0: "Windows Vista / Windows Server 2008",
    35  		1: "Windows 7 / Windows Server 2008 R2",
    36  		2: "Windows 8 / Windows Server 2012",
    37  		3: "Windows 8.1 / Windows Server 2012 R2",
    38  	},
    39  	10: {
    40  		0: "Windows 10 / Windows Server",
    41  	},
    42  }
    43  
    44  func winVersionName(major, minor int) string {
    45  	name := "Unknown"
    46  	if subversions, ok := versions[major]; ok {
    47  		if value, ok := subversions[minor]; ok {
    48  			name = value
    49  		}
    50  	}
    51  	return name
    52  }
    53  
    54  func newOSVersionInfo(major, minor, micro int) *OSVersionInfo {
    55  	return &OSVersionInfo{
    56  		&VersionInfo{
    57  			fmt.Sprintf("%d.%d.%d", major, minor, micro),
    58  			major,
    59  			minor,
    60  			micro,
    61  		},
    62  		winVersionName(major, minor),
    63  	}
    64  }
    65  
    66  // OSVersion returns the system's OS version.
    67  func OSVersion() (*OSVersionInfo, error) {
    68  	if cached, found := sysinfoCache.Get(osVersionInfoCacheKey); found {
    69  		return cached.(*OSVersionInfo), nil
    70  	}
    71  
    72  	if v := os.Getenv(VersionOverrideEnvVar); v != "" {
    73  		vInfo, err := parseVersionInfo(v)
    74  		if err != nil {
    75  			return nil, fmt.Errorf("Could not parse version info: %w", err)
    76  		}
    77  		return &OSVersionInfo{vInfo, "spoofed"}, nil
    78  	}
    79  
    80  	osvi, err := newOSVersionInfoFromRegistry()
    81  	if err == nil {
    82  		sysinfoCache.Set(osVersionInfoCacheKey, osvi, cache.NoExpiration)
    83  		return osvi, nil
    84  	}
    85  	regErr := err
    86  
    87  	osvi, err = newOSVersionInfoFromDLL()
    88  	if err != nil {
    89  		return nil, fmt.Errorf("From DLL error: %v.\nFrom Registry error: %v", err, regErr)
    90  	}
    91  
    92  	sysinfoCache.Set(osVersionInfoCacheKey, osvi, cache.NoExpiration)
    93  	return osvi, nil
    94  }
    95  
    96  func newOSVersionInfoFromRegistry() (*OSVersionInfo, error) {
    97  	keyName := `SOFTWARE\Microsoft\Windows NT\CurrentVersion`
    98  	key, err := registry.OpenKey(registry.LOCAL_MACHINE, keyName, registry.QUERY_VALUE)
    99  	if err != nil {
   100  		return nil, fmt.Errorf("Cannot open registry key %q: %w", keyName, err)
   101  	}
   102  	defer key.Close()
   103  
   104  	keyEntryErrMsgFmt := "Cannot get entry %q at %q: %w"
   105  
   106  	stat, err := key.Stat()
   107  	if err != nil {
   108  		return nil, fmt.Errorf("Cannot stat key %s, err: %w", keyName, err)
   109  	}
   110  
   111  	keys, err := key.ReadValueNames(int(stat.ValueCount))
   112  	if err != nil {
   113  		return nil, fmt.Errorf("Cannot read values for key %s, err: %w", keyName, err)
   114  	}
   115  
   116  	hasVersionInfo := false
   117  	for _, v := range keys {
   118  		if v == "CurrentMajorVersionNumber" {
   119  			hasVersionInfo = true
   120  		}
   121  	}
   122  
   123  	if !hasVersionInfo {
   124  		return newOSVersionInfoFromLegacyRegistry(key)
   125  	}
   126  
   127  	majorEntryName := "CurrentMajorVersionNumber"
   128  	major64, _, err := key.GetIntegerValue(majorEntryName)
   129  	if err != nil {
   130  		return nil, fmt.Errorf(keyEntryErrMsgFmt, majorEntryName, keyName, err)
   131  	}
   132  
   133  	minorEntryName := "CurrentMinorVersionNumber"
   134  	minor64, _, err := key.GetIntegerValue(minorEntryName)
   135  	if err != nil {
   136  		return nil, fmt.Errorf(keyEntryErrMsgFmt, minorEntryName, keyName, err)
   137  	}
   138  
   139  	microEntryName := "CurrentBuild"
   140  	microText, _, err := key.GetStringValue(microEntryName)
   141  	if err != nil {
   142  		return nil, fmt.Errorf(keyEntryErrMsgFmt, microEntryName, keyName, err)
   143  	}
   144  	micro, err := strconv.Atoi(microText)
   145  	if err != nil {
   146  		atoiErr := fmt.Errorf("Cannot convert %q text to integer: %w", microEntryName, err)
   147  		return nil, atoiErr
   148  	}
   149  
   150  	return newOSVersionInfo(int(major64), int(minor64), micro), nil
   151  }
   152  
   153  func newOSVersionInfoFromLegacyRegistry(key registry.Key) (*OSVersionInfo, error) {
   154  	var major64 uint64
   155  	var minor64 uint64
   156  
   157  	productEntryName := "ProductName"
   158  	productName, _, err := key.GetStringValue(productEntryName)
   159  	if err != nil {
   160  		return nil, fmt.Errorf("Cannot get ProductName registry value: %w", err)
   161  	}
   162  
   163  	re := regexp.MustCompile(`\d+`)
   164  	versions := re.FindAllString(productName, -1)
   165  	if len(versions) > 0 {
   166  		if major64, err = strconv.ParseUint(versions[0], 10, 64); err != nil {
   167  			return nil, fmt.Errorf("Invalid int '%v' returned from product name: '%s', error: %v", versions, productName, err)
   168  		}
   169  	}
   170  	if len(versions) > 1 {
   171  		if minor64, err = strconv.ParseUint(versions[1], 10, 64); err != nil {
   172  			return nil, fmt.Errorf("Invalid int '%v' returned from product name: '%s', error: %v", versions, productName, err)
   173  		}
   174  	}
   175  
   176  	return newOSVersionInfo(int(major64), int(minor64), 0), nil
   177  }
   178  
   179  func newOSVersionInfoFromDLL() (*OSVersionInfo, error) {
   180  	dll := windows.NewLazySystemDLL("kernel32.dll")
   181  	version, _, err := dll.NewProc("GetVersion").Call()
   182  	if err != nil {
   183  		return nil, fmt.Errorf("'GetVersion' via kernel32.dll failed: %w", err)
   184  	}
   185  
   186  	major := int(byte(version))
   187  	minor := int(uint8(version >> 8))
   188  	micro := int(uint16(version >> 16))
   189  
   190  	return newOSVersionInfo(major, minor, micro), nil
   191  }
   192  
   193  // Libc returns the system's C library.
   194  func Libc() (*LibcInfo, error) {
   195  	if cached, found := sysinfoCache.Get(libcInfoCacheKey); found {
   196  		return cached.(*LibcInfo), nil
   197  	}
   198  
   199  	// Use Windows powershell in order to query the version information from
   200  	// msvcrt.dll. This works on Windows 7 and higher.
   201  	// Note: cannot easily use version.dll's GetFileVersionInfo function since its
   202  	// return value is a pointer and VerQueryValue is needed in order to fetch a C
   203  	// struct with version info. Manipulating C structs with Go is an exercise in
   204  	// patience.
   205  	windir := os.Getenv("SYSTEMROOT")
   206  	if windir == "" {
   207  		return nil, errors.New("Unable to find system root; %SYSTEMROOT% undefined")
   208  	}
   209  	msvcrt := filepath.Join(windir, "System32", "msvcrt.dll")
   210  	if _, err := os.Stat(msvcrt); err != nil {
   211  		return nil, nil // no libc found
   212  	}
   213  	versionInfo, err := exec.Command("powershell", "-command", "(Get-Item "+msvcrt+").VersionInfo").Output()
   214  	if err != nil {
   215  		return nil, fmt.Errorf("Unable to determine libc version: %s", err)
   216  	}
   217  	regex := regexp.MustCompile(`(\d+)\D(\d+)`)
   218  	parts := regex.FindStringSubmatch(string(versionInfo))
   219  	if len(parts) != 3 {
   220  		return nil, fmt.Errorf("Unable to parse versionInfo string '%s'", versionInfo)
   221  	}
   222  	major, _ := strconv.Atoi(parts[1])
   223  	minor, _ := strconv.Atoi(parts[2])
   224  	info := &LibcInfo{Msvcrt, major, minor}
   225  	sysinfoCache.Set(libcInfoCacheKey, info, cache.NoExpiration)
   226  	return info, nil
   227  }
   228  
   229  // Compilers returns the system's available compilers.
   230  func Compilers() ([]*CompilerInfo, error) {
   231  	if cached, found := sysinfoCache.Get(compilersCacheKey); found {
   232  		return cached.([]*CompilerInfo), nil
   233  	}
   234  
   235  	compilers := []*CompilerInfo{}
   236  
   237  	// Map of compiler commands to CompilerNameInfos.
   238  	var compilerMap = map[string]CompilerNameInfo{
   239  		"gcc.exe": Mingw,
   240  	}
   241  	// Search for MSVC locations and add their C++ compilers to the map.
   242  	if key, err := registry.OpenKey(registry.LOCAL_MACHINE, "Software\\Wow6432Node\\Microsoft\\VisualStudio\\SxS\\VS7", registry.QUERY_VALUE); err == nil {
   243  		// This registry technique works for all MSVC prior to 15.0 (VS2017).
   244  		if valueNames, err := key.ReadValueNames(0); err == nil {
   245  			for _, name := range valueNames {
   246  				if _, err := strconv.ParseFloat(name, 32); err != nil {
   247  					continue
   248  				}
   249  				path, _, err := key.GetStringValue(name)
   250  				if err != nil {
   251  					continue
   252  				}
   253  				cl := filepath.Join(path, "VC", "bin", "cl.exe")
   254  				if _, err = os.Stat(cl); err == nil {
   255  					compilerMap[cl] = Msvc
   256  				}
   257  			}
   258  		}
   259  	}
   260  	for command, nameInfo := range compilerMap {
   261  		major, minor, err := getCompilerVersion([]string{command})
   262  		if err != nil {
   263  			return compilers, err
   264  		} else if major > 0 {
   265  			compilers = append(compilers, &CompilerInfo{nameInfo, major, minor})
   266  		}
   267  	}
   268  
   269  	sysinfoCache.Set(compilersCacheKey, compilers, cache.NoExpiration)
   270  	return compilers, nil
   271  }