-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
25 changed files
with
322 additions
and
403 deletions.
There are no files selected for viewing
2 changes: 1 addition & 1 deletion
2
.../collector/sysinfo/export_windows_test.go → ...r/sysinfo/hardware/export_windows_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
package sysinfo | ||
package hardware | ||
|
||
func WithProductInfo(cmd []string) Options { | ||
return func(o *options) { | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
330 changes: 316 additions & 14 deletions
330
internal/collector/sysinfo/hardware/hardware_windows.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,35 +1,337 @@ | ||
package hardware | ||
|
||
import "log/slog" | ||
import ( | ||
"context" | ||
"fmt" | ||
"log/slog" | ||
"regexp" | ||
"runtime" | ||
"strconv" | ||
"strings" | ||
"time" | ||
|
||
"github.com/ubuntu/ubuntu-insights/internal/cmdutils" | ||
) | ||
|
||
type options struct { | ||
productCmd []string | ||
cpuCmd []string | ||
gpuCmd []string | ||
memoryCmd []string | ||
|
||
diskCmd []string | ||
partitionCmd []string | ||
|
||
screenCmd []string | ||
|
||
arch string | ||
|
||
log *slog.Logger | ||
} | ||
|
||
// defaultOptions returns options for when running under a normal environment. | ||
func defaultOptions() *options { | ||
return &options{} | ||
return &options{ | ||
productCmd: []string{"powershell.exe", "-Command", "Get-CIMInstance", "Win32_ComputerSystem", "|", "Format-List", "-Property", "*"}, | ||
cpuCmd: []string{"powershell.exe", "-Command", "Get-CIMInstance", "Win32_Processor", "|", "Format-List", "-Property", "*"}, | ||
gpuCmd: []string{"powershell.exe", "-Command", "Get-CIMInstance", "Win32_VideoController", "|", "Format-List", "-Property", "*"}, | ||
memoryCmd: []string{"powershell.exe", "-Command", "Get-CIMInstance", "Win32_ComputerSystem", "|", "Format-List", "-Property", "TotalPhysicalMemory"}, | ||
|
||
diskCmd: []string{"powershell.exe", "-Command", "Get-CIMInstance", "Win32_DiskDrive", "|", "Format-List", "-Property", "*"}, | ||
partitionCmd: []string{"powershell.exe", "-Command", "Get-CIMInstance", "Win32_DiskPartition", "|", "Format-List", "-Property", "*"}, | ||
|
||
screenCmd: []string{"powershell.exe", "-Command", "Get-CIMInstance", "Win32_DesktopMonitor", "|", "Format-List", "-Property", "*"}, | ||
|
||
arch: runtime.GOARCH, | ||
|
||
log: slog.Default(), | ||
} | ||
} | ||
|
||
func (s Collector) collectProduct() (product, error) { | ||
return product{}, nil | ||
var usedProductFields = map[string]struct{}{ | ||
"Model": {}, | ||
"Manufacturer": {}, | ||
"SystemSKUNumber": {}, | ||
} | ||
|
||
func (s Collector) collectCPU() (cpu, error) { | ||
return cpu{}, nil | ||
// collectProduct uses Win32_ComputerSystem to find information about the system. | ||
func (s Collector) collectProduct() (product product, err error) { | ||
products, err := s.runWMI(s.opts.productCmd, usedProductFields) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if len(products) > 1 { | ||
s.opts.log.Info("product information more than 1 products", "count", len(products)) | ||
} | ||
|
||
product = products[0] | ||
|
||
product["Family"] = product["SystemSKUNumber"] | ||
delete(product, "SystemSKUNumber") | ||
|
||
product["Vendor"] = product["Manufacturer"] | ||
delete(product, "Manufacturer") | ||
|
||
return product, nil | ||
} | ||
|
||
func (s Collector) collectGPUs() ([]gpu, error) { | ||
return []gpu{}, nil | ||
var usedCPUFields = map[string]struct{}{ | ||
"NumberOfLogicalProcessors": {}, | ||
"NumberOfCores": {}, | ||
"Manufacturer": {}, | ||
"Name": {}, | ||
} | ||
|
||
func (s Collector) collectMemory() (memory, error) { | ||
return memory{}, nil | ||
// collectCPU uses Win32_Processor to collect information about the CPUs. | ||
func (s Collector) collectCPU() (cpu cpu, err error) { | ||
cpus, err := s.runWMI(s.opts.cpuCmd, usedCPUFields) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// we are assuming all CPUs are the same | ||
cpus[0]["Sockets"] = strconv.Itoa(len(cpus)) | ||
|
||
cpus[0]["Architecture"] = s.opts.arch | ||
|
||
return cpus[0], nil | ||
} | ||
|
||
func (s Collector) collectDisks() ([]disk, error) { | ||
return []disk{}, nil | ||
var usedGPUFields = map[string]struct{}{ | ||
"Name": {}, | ||
"InstalledDisplayDrivers": {}, | ||
"AdapterCompatibility": {}, | ||
} | ||
|
||
func (s Collector) collectScreens() ([]screen, error) { | ||
return []screen{}, nil | ||
// collectGPUs uses Win32_VideoController to collect information about the GPUs. | ||
func (s Collector) collectGPUs() (gpus []gpu, err error) { | ||
gpus, err = s.runWMI(s.opts.gpuCmd, usedGPUFields) | ||
if err != nil { | ||
return gpus, err | ||
} | ||
|
||
for _, g := range gpus { | ||
// InstalledDisplayDrivers is a comma separated list of paths to drivers | ||
v, _, _ := strings.Cut(g["InstalledDisplayDrivers"], ",") | ||
vs := strings.Split(v, `\`) | ||
|
||
g["Driver"] = vs[len(vs)-1] | ||
delete(g, "InstalledDisplayDrivers") | ||
|
||
g["Vendor"] = g["AdapterCompatibility"] | ||
delete(g, "AdapterCompatibility") | ||
} | ||
|
||
return gpus, nil | ||
} | ||
|
||
var usedMemoryFields = map[string]struct{}{ | ||
"TotalPhysicalMemory": {}, | ||
} | ||
|
||
// collectMemory uses Win32_ComputerSystem to collect information about RAM. | ||
func (s Collector) collectMemory() (mem memory, err error) { | ||
oses, err := s.runWMI(s.opts.memoryCmd, usedMemoryFields) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
var size = 0 | ||
for _, os := range oses { | ||
sm := os["TotalPhysicalMemory"] | ||
v, err := strconv.Atoi(sm) | ||
if err != nil { | ||
s.opts.log.Warn("memory info contained non-integer memory", "value", sm) | ||
continue | ||
} | ||
if v < 0 { | ||
s.opts.log.Warn("memory info contained negative memory", "value", sm) | ||
continue | ||
} | ||
size += v | ||
} | ||
|
||
return memory{ | ||
"MemTotal": size, | ||
}, nil | ||
} | ||
|
||
var usedDiskFields = map[string]struct{}{ | ||
"Name": {}, | ||
"Size": {}, | ||
"Partitions": {}, | ||
} | ||
|
||
var usedPartitionFields = map[string]struct{}{ | ||
"DiskIndex": {}, | ||
"Index": {}, | ||
"Name": {}, | ||
"Size": {}, | ||
} | ||
|
||
// collectDisks uses Win32_DiskDrive and Win32_DiskPartition to collect information about disks. | ||
func (s Collector) collectDisks() (blks []disk, err error) { | ||
disks, err := s.runWMI(s.opts.diskCmd, usedDiskFields) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
const maxPartitions = 128 | ||
|
||
blks = make([]disk, 0, len(disks)) | ||
for _, d := range disks { | ||
parts, err := strconv.Atoi(d["Partitions"]) | ||
if err != nil { | ||
s.opts.log.Warn("disk partitions was not an integer", "error", err) | ||
parts = 0 | ||
} | ||
if parts < 0 { | ||
s.opts.log.Warn("disk partitions was negative", "value", parts) | ||
parts = 0 | ||
} | ||
if parts > maxPartitions { | ||
s.opts.log.Warn("disk partitions too large", "value", parts) | ||
parts = maxPartitions | ||
} | ||
|
||
c := disk{ | ||
Name: d["Name"], | ||
Size: d["Size"], | ||
Partitions: make([]disk, parts), | ||
} | ||
for i := range c.Partitions { | ||
c.Partitions[i].Partitions = []disk{} | ||
} | ||
blks = append(blks, c) | ||
} | ||
|
||
parts, err := s.runWMI(s.opts.partitionCmd, usedPartitionFields) | ||
if err != nil { | ||
s.opts.log.Warn("can't get partitions", "error", err) | ||
return blks, nil | ||
} | ||
|
||
for _, p := range parts { | ||
d, err := strconv.Atoi(p["DiskIndex"]) | ||
if err != nil { | ||
s.opts.log.Warn("partition disk index was not an integer", "error", err) | ||
continue | ||
} | ||
if d < 0 { | ||
s.opts.log.Warn("partition disk index was negative", "value", d) | ||
continue | ||
} | ||
if d >= len(blks) { | ||
s.opts.log.Warn("partition disk index was larger than disks", "value", d) | ||
continue | ||
} | ||
|
||
idx, err := strconv.Atoi(p["Index"]) | ||
if err != nil { | ||
s.opts.log.Warn("partition index was not an integer", "error", err, "disk", d) | ||
continue | ||
} | ||
if idx < 0 { | ||
s.opts.log.Warn("partition index was negative", "value", idx, "disk", d) | ||
continue | ||
} | ||
if idx >= len(blks[d].Partitions) { | ||
s.opts.log.Warn("partition index was larger than partitions", "value", idx, "disk", d) | ||
continue | ||
} | ||
|
||
blks[d].Partitions[idx] = disk{ | ||
Name: p["Name"], | ||
Size: p["Size"], | ||
Partitions: []disk{}, | ||
} | ||
} | ||
|
||
return blks, nil | ||
} | ||
|
||
var usedScreenFields = map[string]struct{}{ | ||
"Name": {}, | ||
"ScreenWidth": {}, | ||
"ScreenHeight": {}, | ||
} | ||
|
||
// collectScreens uses Win32_DesktopMonitor to collect information about screens. | ||
func (s Collector) collectScreens() (screens []screen, err error) { | ||
monitors, err := s.runWMI(s.opts.screenCmd, usedScreenFields) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
screens = make([]screen, 0, len(monitors)) | ||
|
||
for _, s := range monitors { | ||
screens = append(screens, screen{ | ||
Name: s["Name"], | ||
Resolution: fmt.Sprintf("%sx%s", s["ScreenWidth"], s["ScreenHeight"]), | ||
}) | ||
} | ||
|
||
return screens, nil | ||
} | ||
|
||
// wmiEntryRegex matches the key and value (if any) from gwmi output. | ||
// For example: "Status : OK " matches and has "Status", "OK". | ||
// Or: "DitherType:" matches and has "DitherType", "". | ||
// However: " : OK" does not match. | ||
var wmiEntryRegex = regexp.MustCompile(`(?m)^\s*(\S+)\s*:[^\S\n]*(.*?)\s*$`) | ||
|
||
var wmiReplaceRegex = regexp.MustCompile(`\r?\n\s*`) | ||
|
||
// wmiSplitRegex splits on two consecutive newlines, but \r needs special handling. | ||
var wmiSplitRegex = regexp.MustCompile(`\r?\n\r?\n`) | ||
|
||
// runWMI runs the cmdlet specified by args and only includes fields in the filter. | ||
func (s Collector) runWMI(args []string, filter map[string]struct{}) (out []map[string]string, err error) { | ||
defer func() { | ||
if err == nil && len(out) == 0 { | ||
err = fmt.Errorf("%v output contained no sections", args) | ||
} | ||
}() | ||
|
||
if len(filter) == 0 { | ||
return nil, fmt.Errorf("empty filter will always produce nothing for cmdlet %v", args) | ||
} | ||
|
||
stdout, stderr, err := cmdutils.RunWithTimeout(context.Background(), 15*time.Second, args[0], args[1:]...) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if stderr.Len() > 0 { | ||
s.opts.log.Info(fmt.Sprintf("%v output to stderr", args), "stderr", stderr) | ||
} | ||
|
||
sections := wmiSplitRegex.Split(stdout.String(), -1) | ||
out = make([]map[string]string, 0, len(sections)) | ||
|
||
for _, section := range sections { | ||
if section == "" { | ||
continue | ||
} | ||
|
||
entries := wmiEntryRegex.FindAllStringSubmatch(section, -1) | ||
if len(entries) == 0 { | ||
s.opts.log.Info(fmt.Sprintf("%v output has malformed section", args), "section", section) | ||
continue | ||
} | ||
|
||
v := make(map[string]string, len(filter)) | ||
for _, e := range entries { | ||
if _, ok := filter[e[1]]; !ok { | ||
continue | ||
} | ||
|
||
// Get-WmiObject injects newlines and whitespace into values for formatting | ||
v[e[1]] = wmiReplaceRegex.ReplaceAllString(e[2], "") | ||
} | ||
|
||
out = append(out, v) | ||
} | ||
|
||
return out, nil | ||
} |
2 changes: 1 addition & 1 deletion
2
...collector/sysinfo/sysinfo_windows_test.go → ...sysinfo/hardware/hardware_windows_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
package sysinfo_test | ||
package hardware_test | ||
|
||
import ( | ||
"flag" | ||
|
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Oops, something went wrong.