internal/osinfo/osinfo_windows.go (158 lines of code) (raw):

// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build windows package osinfo import ( "errors" "fmt" "os" "path/filepath" "strings" "syscall" "unsafe" "github.com/GoogleCloudPlatform/galog" "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" ) var ( versionDLL = windows.NewLazySystemDLL("version.dll") kernel32DLL = windows.NewLazySystemDLL("kernel32.dll") // https://learn.microsoft.com/en-us/windows/win32/api/winver/nf-winver-getfileversioninfosizew procGetFileVersionInfoSizeW = versionDLL.NewProc("GetFileVersionInfoSizeW") // https://learn.microsoft.com/en-us/windows/win32/api/winver/nf-winver-getfileversioninfow procGetFileVersionInfoW = versionDLL.NewProc("GetFileVersionInfoW") // https://learn.microsoft.com/en-us/windows/win32/api/winver/nf-winver-verqueryvaluew procVerQueryValueW = versionDLL.NewProc("VerQueryValueW") // https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getnativesysteminfo procGetNativeSystemInfo = kernel32DLL.NewProc("GetNativeSystemInfo") ) // systemInfo contains information about the computer system. // https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/ns-sysinfoapi-system_info type systemInfo struct { wProcessorArchitecture uint16 wReserved uint16 dwPageSize uint32 lpMinimumApplicationAddress uintptr lpMaximumApplicationAddress uintptr dwActiveProcessorMask uintptr dwNumberOfProcessors uint32 dwProcessorType uint32 dwAllocationGranularity uint32 wProcessorLevel uint16 wProcessorRevision uint16 } // architecture returns the architecture of the system. func architecture() string { var info systemInfo procGetNativeSystemInfo.Call(uintptr(unsafe.Pointer(&info))) arch := uint(info.wProcessorArchitecture) // https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/ns-sysinfoapi-system_info#members switch arch { case 0: return "x86" case 5: return "arm" case 6: return "ia-64" case 9: return "x64" case 12: return "arm64" default: return "unknown" } } // translation returns the language and code page identifier from the provided // version-information block. func translation(block []byte) (string, error) { var start uint var length uint blockStart := uintptr(unsafe.Pointer(&block[0])) // If the specified name does not exist or the specified resource is not valid, the return value is zero. if exists, _, _ := procVerQueryValueW.Call( blockStart, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(`\VarFileInfo\Translation`))), uintptr(unsafe.Pointer(&start)), uintptr(unsafe.Pointer(&length)), ); exists == 0 { return "", errors.New("zero return code from VerQueryValueW indicates failure") } begin := int(start) - int(blockStart) // For translation data length is bytes. trans := block[begin : begin+int(length)] // Each 'translation' is 4 bytes long (2 16-bit sections), we just want the // first one for simplicity. t := make([]byte, 4) // 16-bit language ID little endian // https://msdn.microsoft.com/en-us/library/windows/desktop/dd318693(v=vs.85).aspx t[0], t[1] = trans[1], trans[0] // 16-bit code page ID little endian // https://msdn.microsoft.com/en-us/library/windows/desktop/dd317756(v=vs.85).aspx t[2], t[3] = trans[3], trans[2] return fmt.Sprintf("%x", t), nil } // stringFileInfo returns the string value file info name specific to the language and code page indicated. func stringFileInfo(block []byte, langCodePage, name string) (string, error) { var start uint var length uint blockStart := uintptr(unsafe.Pointer(&block[0])) // If the specified name does not exist or the specified resource is not valid, the return value is zero. if exists, _, _ := procVerQueryValueW.Call( blockStart, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(fmt.Sprintf(`\StringFileInfo\%s\%s`, langCodePage, name)))), uintptr(unsafe.Pointer(&start)), uintptr(unsafe.Pointer(&length)), ); exists == 0 { return "", errors.New("zero return code from VerQueryValueW indicates failure") } begin := int(start) - int(blockStart) // For version information length is characters (UTF16). result := block[begin : begin+int(2*length)] // Result is UTF16LE. u16s := make([]uint16, length) for i := range u16s { u16s[i] = uint16(result[i*2+1])<<8 | uint16(result[i*2]) } return syscall.UTF16ToString(u16s), nil } func version(block []byte, langCodePage string) (string, string, error) { ver, err := stringFileInfo(block, langCodePage, "FileVersion") if err != nil { return "", "", err } rel, err := stringFileInfo(block, langCodePage, "ProductVersion") return ver, rel, err } func kernelInfo() (string, string, error) { root := os.Getenv("SystemRoot") if root == "" { root = `C:\Windows` } path := filepath.Join(root, "System32", "ntoskrnl.exe") if _, err := os.Stat(path); err != nil { return "", "", err } pPtr := unsafe.Pointer(syscall.StringToUTF16Ptr(path)) size, _, _ := procGetFileVersionInfoSizeW.Call(uintptr(pPtr)) if size <= 0 { return "", "", errors.New("GetFileVersionInfoSize call failed, data size can not be 0") } info := make([]byte, size) // If the function fails, the return value is zero. if success, _, _ := procGetFileVersionInfoW.Call( uintptr(pPtr), 0, uintptr(len(info)), uintptr(unsafe.Pointer(&info[0])), ); success == 0 { return "", "", errors.New("zero return code from GetFileVersionInfoW indicates failure") } // This should be something like 040904b0 for US English UTF16LE. langCodePage, err := translation(info) if err != nil { return "", "", fmt.Errorf("getTranslation() error: %w", err) } return version(info, langCodePage) } // Read returns OSInfo on the running system. func Read() OSInfo { var osInfo OSInfo osInfo.OS = "windows" kVersion, kRelease, err := kernelInfo() if err != nil { galog.Warnf("getKernelInfo() error: %v", err) return osInfo } osInfo.KernelVersion = kVersion osInfo.KernelRelease = kRelease osInfo.Architecture = architecture() // SOFTWARE\Microsoft\Windows NT\CurrentVersion\CurrentVersion is always 6.3 now, so we get os version from kernel release. vs := strings.Split(kRelease, ".") if len(vs) == 4 { osInfo.VersionID = strings.Join(vs[:3], ".") } k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE) if err != nil { galog.Warnf("registry.OpenKey error: %v", err) return osInfo } defer k.Close() productName, _, err := k.GetStringValue("ProductName") if err != nil { galog.Warnf("GetStringValue('ProductName') error: %v", err) return osInfo } osInfo.PrettyName = productName return osInfo }