//go:build windows package agent // probe_windows.go is the Windows sampler for the probe collector, // feeding the same Facts/Metrics contract and accounting discipline // as the Linux sampler (see probe.go) from the Win32 surface: // // - CPU%: GetSystemTimes differenced through the shared cpuPercent // (kernel time includes idle, exactly like /proc/stat jiffies); // - memory: GlobalMemoryStatusEx, used = TotalPhys - AvailPhys // (the free(1) "available" column's spirit); swap is the pagefile // pair (TotalPageFile/AvailPageFile); // - disk: fixed drives only (GetDriveType == DRIVE_FIXED), summed // like df: used = total - free bytes (GetDiskFreeSpaceEx's // TotalFree, NOT the caller-available figure, which discounts // quotas the way f_bavail discounts the ext reserve); // - network: GetIfTable2 rows filtered to OperStatus Up, ifType // Ethernet (6) or IEEE80211 (71), non-loopback. This approximates // the Linux three-layer filter: Windows exposes no stacking // topology in the MIB, but tunnel/virtual adapters report their // own ifTypes (tunnel 131, loopback 24, ...), so an allowlist of // physical media lands on the same counted set in practice. The // cost of the difference: a physical-medium bond/virtual switch // vNIC presenting as Ethernet IS counted here even where Linux // layer 3 would have skipped it; // - boot identity: the registry MachineGuid stands in for the // kernel boot_id; the counted-set FNV fingerprint is shared, so a // NIC change re-baselines exactly like on Linux; // - uptime: GetTickCount64 (via x/sys' DurationSinceBoot wrapper); // - load averages, TCP/UDP connection counts have no exported // x/sys/windows (v0.47.0) source — GetSystemTimes-style raw // counters exist for CPU/memory, but the package exports no // GetTcpTable2/GetUdpTable2 wrapper, so per the independent- // degradation discipline Load stays zero and TCP/UDP stay 0 // until a wrapper exists; // - every call fails soft to zeros, like an unreadable /proc file. import ( "net" "os" "runtime" "strings" "time" "unsafe" "github.com/mhsanaei/3x-ui/v3/internal/config" "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" ) // kernel32 procs x/sys/windows v0.47.0 does not wrap. var ( kernel32DLL = windows.NewLazySystemDLL("kernel32.dll") procGetSystemTimes = kernel32DLL.NewProc("GetSystemTimes") procGlobalMemoryStatus = kernel32DLL.NewProc("GlobalMemoryStatusEx") iphlpapiDLL = windows.NewLazySystemDLL("iphlpapi.dll") procFreeMibIfTable2 = iphlpapiDLL.NewProc("FreeMibIfTable2") ) func sampleFacts() *ProbeFacts { memTotal, _, swapTotal, _ := winMemory() diskTotal, _ := winDiskUsage() v4, v6 := winAddresses() return &ProbeFacts{ Hostname: winHostname(), OS: winOSName(), Kernel: winKernelBuild(), Arch: runtime.GOARCH, Virt: winVirt(), CPUName: winCPUName(), CPUCores: uint32(runtime.NumCPU()), MemTotal: memTotal, SwapTotal: swapTotal, DiskTotal: diskTotal, AgentVersion: config.GetAgentDisplayVersion(), IPv4: v4, IPv6: v6, } } func collectMetrics() *ProbeMetrics { probeColl.mu.Lock() defer probeColl.mu.Unlock() memTotal, memUsed, swapTotal, swapUsed := winMemory() diskTotal, diskUsed := winDiskUsage() rows := winIfaceRows() all := make([]netCounters, 0, len(rows)) counted := make([]netCounters, 0, len(rows)) for _, r := range rows { c := netCounters{name: r.name, rx: r.rx, tx: r.tx} all = append(all, c) if winIfaceCounts(r) { counted = append(counted, c) } } if pin := statIfacePinValue(); pin != "" { counted = pinnedInterfaces(all, pin) } var rxTotal, txTotal uint64 names := make([]string, 0, len(counted)) for _, c := range counted { rxTotal += c.rx txTotal += c.tx names = append(names, c.name) } rxRate, txRate := probeColl.netRate(counted, time.Now()) return &ProbeMetrics{ BootID: bootEpoch(winMachineGUID(), names), Uptime: uint64(windows.DurationSinceBoot().Seconds()), CPU: probeColl.cpuPercent(winCPUSample()), Load: [3]float32{}, MemTotal: memTotal, MemUsed: memUsed, SwapTotal: swapTotal, SwapUsed: swapUsed, DiskTotal: diskTotal, DiskUsed: diskUsed, NetRxTotal: rxTotal, NetTxTotal: txTotal, NetRx: rxRate, NetTx: txRate, TCP: 0, // no exported x/sys wrapper (see header) UDP: 0, // no exported x/sys wrapper (see header) Procs: winProcCount(), } } // --- registry helpers -------------------------------------------------------- func regGetString(root registry.Key, path, name string) string { k, err := registry.OpenKey(root, path, registry.QUERY_VALUE) if err != nil { return "" } defer k.Close() v, _, err := k.GetStringValue(name) if err != nil { return "" } return strings.TrimSpace(v) } func winMachineGUID() string { return regGetString(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Cryptography`, "MachineGuid") } func winOSName() string { const key = `SOFTWARE\Microsoft\Windows NT\CurrentVersion` name := regGetString(registry.LOCAL_MACHINE, key, "ProductName") if name == "" { return "Windows" } // DisplayVersion ("23H2") disambiguates: ProductName still says // "Windows 10" on most Windows 11 installs. if dv := regGetString(registry.LOCAL_MACHINE, key, "DisplayVersion"); dv != "" { name += " " + dv } return name } func winKernelBuild() string { const key = `SOFTWARE\Microsoft\Windows NT\CurrentVersion` if b := regGetString(registry.LOCAL_MACHINE, key, "CurrentBuild"); b != "" { return b } return "unknown" } func winCPUName() string { if n := regGetString(registry.LOCAL_MACHINE, `HARDWARE\DESCRIPTION\System\CentralProcessor\0`, "ProcessorNameString"); n != "" { return n } return "unknown" } // winVirt is best-effort: the BIOS strings Windows itself surfaces. // No match reads "unknown" (Windows guests of a hypervisor that hides // them are indistinguishable from bare metal this way). func winVirt() string { const bios = `HARDWARE\DESCRIPTION\System\BIOS` hay := strings.ToLower(regGetString(registry.LOCAL_MACHINE, bios, "SystemManufacturer") + " " + regGetString(registry.LOCAL_MACHINE, bios, "SystemProductName")) for _, m := range []struct{ word, name string }{ {"vmware", "vmware"}, {"virtualbox", "virtualbox"}, {"qemu", "kvm"}, {"kvm", "kvm"}, {"xen", "xen"}, {"virtual machine", "hyper-v"}, {"amazon ec2", "kvm"}, {"google", "kvm"}, {"bochs", "bochs"}, } { if strings.Contains(hay, m.word) { return m.name } } return "unknown" } func winHostname() string { if h, err := os.Hostname(); err == nil && h != "" { return h } return "unknown" } // --- cpu / memory -------------------------------------------------------------- // filetime100ns flattens a FILETIME (100ns ticks) for the shared differ. func filetime100ns(ft windows.Filetime) uint64 { return uint64(ft.HighDateTime)<<32 | uint64(ft.LowDateTime) } // winCPUSample reads the cumulative system times as one cpuSample: // total = kernel + user (kernel already includes idle, like jiffies). func winCPUSample() *cpuSample { var idle, kernel, user windows.Filetime r, _, _ := procGetSystemTimes.Call( uintptr(unsafe.Pointer(&idle)), uintptr(unsafe.Pointer(&kernel)), uintptr(unsafe.Pointer(&user))) if r == 0 { return nil } return &cpuSample{ total: filetime100ns(kernel) + filetime100ns(user), idle: filetime100ns(idle), } } type memoryStatusEx struct { Length uint32 MemoryLoad uint32 TotalPhys uint64 AvailPhys uint64 TotalPageFile uint64 AvailPageFile uint64 TotalVirtual uint64 AvailVirtual uint64 AvailExtendedVirtual uint64 } // usedFromTotalAvail is the used-column discipline as a pure // function: used = total - avail, saturating at 0. func usedFromTotalAvail(total, avail uint64) uint64 { if avail > total { return 0 } return total - avail } func winMemory() (total, used, swapTotal, swapUsed uint64) { var ms memoryStatusEx ms.Length = uint32(unsafe.Sizeof(ms)) r, _, _ := procGlobalMemoryStatus.Call(uintptr(unsafe.Pointer(&ms))) if r == 0 { return 0, 0, 0, 0 } return ms.TotalPhys, usedFromTotalAvail(ms.TotalPhys, ms.AvailPhys), ms.TotalPageFile, usedFromTotalAvail(ms.TotalPageFile, ms.AvailPageFile) } // --- disk (df口径) ----------------------------------------------------------- // driveSpace is one fixed drive's GetDiskFreeSpaceEx result. type driveSpace struct{ total, free uint64 } // sumDriveSpace aggregates fixed drives the df way: used = total - // free (the caller-available figure is deliberately not consulted). func sumDriveSpace(ds []driveSpace) (total, used uint64) { for _, d := range ds { total += d.total used += usedFromTotalAvail(d.total, d.free) } return total, used } // splitDriveStrings decodes the GetLogicalDriveStrings multi-string // buffer (NUL-separated roots, double-NUL terminated). Pure, so the // layout is unit-testable off-device. func splitDriveStrings(buf []uint16) []string { var out []string start := 0 for i, c := range buf { if c != 0 { continue } if i == start { break // double NUL: end of the list } out = append(out, windows.UTF16ToString(buf[start:i])) start = i + 1 } return out } // fixedDriveRoots lists roots ("C:\") of DRIVE_FIXED drives. func fixedDriveRoots() []string { n, err := windows.GetLogicalDriveStrings(0, nil) if err != nil || n == 0 { return nil } buf := make([]uint16, n) n, err = windows.GetLogicalDriveStrings(n, &buf[0]) if err != nil { return nil } var roots []string for _, s := range splitDriveStrings(buf[:n]) { p, err := windows.UTF16PtrFromString(s) if err != nil { continue } if windows.GetDriveType(p) == windows.DRIVE_FIXED { roots = append(roots, s) } } return roots } func winDiskUsage() (total, used uint64) { var spaces []driveSpace for _, root := range fixedDriveRoots() { p, err := windows.UTF16PtrFromString(root) if err != nil { continue } var freeToCaller, totalBytes, totalFree uint64 if err := windows.GetDiskFreeSpaceEx(p, &freeToCaller, &totalBytes, &totalFree); err != nil { continue } spaces = append(spaces, driveSpace{total: totalBytes, free: totalFree}) } return sumDriveSpace(spaces) } // --- network: counted set ------------------------------------------------------ // winIfaceRow is the filter-relevant slice of one MIB_IF_ROW2. type winIfaceRow struct { name string ifType uint32 operUp bool rx, tx uint64 } // winIfaceCounts applies the counted-set rule to one row (see the // file header for how it approximates the Linux three-layer filter). func winIfaceCounts(r winIfaceRow) bool { if !r.operUp { return false } if r.ifType == windows.IF_TYPE_SOFTWARE_LOOPBACK { return false } return r.ifType == windows.IF_TYPE_ETHERNET_CSMACD || r.ifType == windows.IF_TYPE_IEEE80211 } // winIfaceRows reads the full MIB_IF_TABLE2 once; collectMetrics // derives both the all-interfaces list (for the operator pin) and the // counted set from it. func winIfaceRows() []winIfaceRow { var table *windows.MibIfTable2 if err := windows.GetIfTable2Ex(windows.MibIfTableNormal, &table); err != nil || table == nil { return nil } defer procFreeMibIfTable2.Call(uintptr(unsafe.Pointer(table))) n := int(table.NumEntries) if n == 0 { return nil } rows := unsafe.Slice(&table.Table[0], n) out := make([]winIfaceRow, 0, n) for i := range rows { r := &rows[i] out = append(out, winIfaceRow{ name: windows.UTF16ToString(r.Alias[:]), ifType: r.Type, operUp: r.OperStatus == windows.IfOperStatusUp, rx: r.InOctets, tx: r.OutOctets, }) } return out } // --- processes ----------------------------------------------------------------- func winProcCount() uint32 { snap, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0) if err != nil { return 0 } defer windows.CloseHandle(snap) var pe windows.ProcessEntry32 pe.Size = uint32(unsafe.Sizeof(pe)) if err := windows.Process32First(snap, &pe); err != nil { return 0 } var n uint32 = 1 for { if err := windows.Process32Next(snap, &pe); err != nil { break } n++ } return n } // --- addresses ------------------------------------------------------------------- // winAddresses picks one address per family from up, non-loopback // interfaces: public preferred over private, like the Linux picker — // only without its /proc inputs (no operstate file, no temporary- // address flags; net.Interfaces' own flags do the loopback cut). func winAddresses() (string, string) { ifaces, err := net.Interfaces() if err != nil { return "", "" } var cands []net.IP for _, ifc := range ifaces { if ifc.Flags&net.FlagUp == 0 || ifc.Flags&net.FlagLoopback != 0 { continue } addrs, err := ifc.Addrs() if err != nil { continue } for _, addr := range addrs { var ip net.IP switch v := addr.(type) { case *net.IPNet: ip = v.IP case *net.IPAddr: ip = v.IP } if ip == nil || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsLoopback() { continue } cands = append(cands, ip) } } pick := func(v6 bool) string { var best net.IP for _, ip := range cands { if (ip.To4() == nil) != v6 { continue } if best == nil { best = ip continue } if isPublicIP(ip) && !isPublicIP(best) { best = ip } } if best == nil { return "" } return best.String() } return pick(false), pick(true) }