Skip to content

Commit

Permalink
[Windows] Apply restructure
Browse files Browse the repository at this point in the history
  • Loading branch information
Sploder12 committed Feb 4, 2025
1 parent d3d10bc commit bd38068
Show file tree
Hide file tree
Showing 25 changed files with 322 additions and 403 deletions.
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) {
Expand Down
8 changes: 4 additions & 4 deletions internal/collector/sysinfo/hardware/hardware.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ type Info struct {
Screens []screen
}

type product map[string]string
type cpu map[string]string
type gpu map[string]string
type memory map[string]int
type product = map[string]string
type cpu = map[string]string
type gpu = map[string]string
type memory = map[string]int

// DiskInfo contains information of a disk or partition.
type disk struct {
Expand Down
330 changes: 316 additions & 14 deletions internal/collector/sysinfo/hardware/hardware_windows.go
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
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package sysinfo_test
package hardware_test

import (
"flag"
Expand Down
Loading

0 comments on commit bd38068

Please sign in to comment.