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 }