diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index efe4600..8271114 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -24,7 +24,7 @@ jobs: run: | git config --global user.name 'github-actions' git config --global user.email 'github-actions@github.com' - TAG="v0.0.6-$(date +'%Y%m%d%H%M%S')" + TAG="v0.0.7-$(date +'%Y%m%d%H%M%S')" git tag $TAG git push origin $TAG env: diff --git a/README.md b/README.md index dc66a7f..ffdfeac 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,10 @@ Include: https://github.com/oneclickvirt/gostun - [x] 使用```sysctl```获取CPU信息-特化适配freebsd、openbsd系统 - [x] 适配```MacOS```与```Windows```系统的信息查询 - [x] 部分Windows10系统下打勾打叉编码错误显示,已判断是Win时使用Y/N显示而不是勾叉 +- [x] 检测GPU相关信息,参考[ghw](https://github.com/jaypipes/ghw) ## TODO -- [ ] 检测GPU相关信息,可参考[ghw](https://github.com/jaypipes/ghw) - [ ] CPUCache的信息需要矫正 - [ ] 纯IPV6环境下使用cdn反代获取平台信息 diff --git a/go.mod b/go.mod index f190f0e..ace43b7 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ go 1.22.4 require ( github.com/imroc/req/v3 v3.43.7 + github.com/jaypipes/ghw v0.12.0 + github.com/json-iterator/go v1.1.12 github.com/libp2p/go-nat v0.2.0 github.com/oneclickvirt/defaultset v0.0.2-20240624082446 github.com/oneclickvirt/gostun v0.0.3-20240702054621 @@ -13,8 +15,10 @@ require ( ) require ( + github.com/StackExchange/wmi v1.2.1 // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/cloudflare/circl v1.3.9 // indirect + github.com/ghodss/yaml v1.0.0 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/gopacket v1.1.19 // indirect @@ -23,16 +27,21 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/huin/goupnp v1.2.0 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect + github.com/jaypipes/pcidb v1.0.0 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/koron/go-ssdp v0.0.4 // indirect github.com/libp2p/go-netroute v0.2.1 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/onsi/ginkgo/v2 v2.19.0 // indirect github.com/pion/dtls/v2 v2.2.7 // indirect github.com/pion/logging v0.2.2 // indirect github.com/pion/stun/v2 v2.0.0 // indirect github.com/pion/transport/v2 v2.2.1 // indirect github.com/pion/transport/v3 v3.0.1 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/quic-go v0.45.1 // indirect @@ -50,4 +59,6 @@ require ( golang.org/x/sync v0.7.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/tools v0.22.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + howett.net/plist v1.0.0 // indirect ) diff --git a/go.sum b/go.sum index 72131cf..039aee2 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= +github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE= @@ -5,8 +7,11 @@ github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= @@ -14,6 +19,7 @@ github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/pprof v0.0.0-20240625030939-27f56978b8b0 h1:e+8XbKB6IMn8A4OAyZccO4pYfB3s7bt6azNIPE7AnPg= @@ -29,16 +35,33 @@ github.com/imroc/req/v3 v3.43.7 h1:dOcNb9n0X83N5/5/AOkiU+cLhzx8QFXjv5MhikazzQA= github.com/imroc/req/v3 v3.43.7/go.mod h1:SQIz5iYop16MJxbo8ib+4LnostGCok8NQf8ToyQc2xA= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jaypipes/ghw v0.12.0 h1:xU2/MDJfWmBhJnujHY9qwXQLs3DBsf0/Xa9vECY0Tho= +github.com/jaypipes/ghw v0.12.0/go.mod h1:jeJGbkRB2lL3/gxYzNYzEDETV1ZJ56OKr+CSeSEym+g= +github.com/jaypipes/pcidb v1.0.0 h1:vtZIfkiCUE42oYbJS0TAq9XSfSmcsgo9IdxSm9qzYU8= +github.com/jaypipes/pcidb v1.0.0/go.mod h1:TnYUvqhPBzCKnH34KrIX22kAeEbDCSRJ9cqLRCuNDfk= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0= github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/libp2p/go-nat v0.2.0 h1:Tyz+bUFAYqGyJ/ppPPymMGbIgNRH+WqC5QrT5fKrrGk= github.com/libp2p/go-nat v0.2.0/go.mod h1:3MJr+GRpRkyT65EpVPBstXLvOlAPzUVlG6Pwg9ohLJk= github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU= github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/oneclickvirt/defaultset v0.0.2-20240624082446 h1:5Pg3mK/u/vQvSz7anu0nxzrNdELi/AcDAU1mMsmPzyc= github.com/oneclickvirt/defaultset v0.0.2-20240624082446/go.mod h1:e9Jt4tf2sbemCtc84/XgKcHy9EZ2jkc5x2sW1NiJS+E= github.com/oneclickvirt/gostun v0.0.3-20240702054621 h1:IE89eEYV9TJbF94SakQDAxTLIaqX+Tb6ZhJ/CCIP+90= @@ -57,6 +80,8 @@ github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= @@ -76,6 +101,7 @@ github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnj github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -171,6 +197,13 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= +howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= diff --git a/model/model.go b/model/model.go index f3914f9..c50b266 100644 --- a/model/model.go +++ b/model/model.go @@ -1,6 +1,6 @@ package model -const BasicsVersion = "v0.0.6" +const BasicsVersion = "v0.0.7" var EnableLoger bool @@ -119,6 +119,7 @@ type DiskInfo struct { type SystemInfo struct { CpuInfo + GPUInfo MemoryInfo DiskInfo Platform string // 系统名字 Distro1 @@ -135,6 +136,11 @@ type SystemInfo struct { TcpAccelerationMethod string // TCP拥塞控制 } +type GPUInfo struct { + GpuModel string + GpuStats string +} + type Win32_Processor struct { L2CacheSize uint32 L3CacheSize uint32 @@ -153,7 +159,7 @@ type Win32CacheMemory struct { } type Win32_VFE struct { - VirtualizationFirmwareEnabled bool + VirtualizationFirmwareEnabled bool } type Win32_ComputerSystem struct { diff --git a/system/gpu.go b/system/gpu.go new file mode 100644 index 0000000..ab75694 --- /dev/null +++ b/system/gpu.go @@ -0,0 +1,87 @@ +package system + +import ( + "fmt" + "math" + "sync" + "sync/atomic" + "time" + + "github.com/oneclickvirt/basics/model" + "github.com/oneclickvirt/basics/system/gpu" + gpustat "github.com/oneclickvirt/basics/system/gpu/stat" + . "github.com/oneclickvirt/defaultset" +) + +var updateGPUStatus int32 +var gpuStat uint64 + +// 获取设备数据的最大尝试次数 +const maxDeviceDataFetchAttempts = 3 + +// 获取主机数据的尝试次数,Key 为 Host 的属性名 +var hostDataFetchAttempts = map[string]int{ + "CPU": 0, + "GPU": 0, +} + +// 获取状态数据的尝试次数,Key 为 HostState 的属性名 +var statDataFetchAttempts = map[string]int{ + "CPU": 0, + "Load": 0, + "GPU": 0, + "Temperatures": 0, +} + +func atomicStoreFloat64(x *uint64, v float64) { + atomic.StoreUint64(x, math.Float64bits(v)) +} + +func updateGPUStat(gpuStat *uint64, wg *sync.WaitGroup) { + if model.EnableLoger { + InitLogger() + defer Logger.Sync() + } + defer wg.Done() + if !atomic.CompareAndSwapInt32(&updateGPUStatus, 0, 1) { + return + } + defer atomic.StoreInt32(&updateGPUStatus, 0) + + for statDataFetchAttempts["GPU"] < maxDeviceDataFetchAttempts { + gs, err := gpustat.GetGPUStat() + if err != nil { + statDataFetchAttempts["GPU"]++ + if model.EnableLoger { + Logger.Info(fmt.Sprintf("gpustat.GetGPUStat error: %s, attempt: %d", err.Error(), statDataFetchAttempts["GPU"])) + } + time.Sleep(1 * time.Second) // 等待一段时间再重试 + } else { + statDataFetchAttempts["GPU"] = 0 + atomicStoreFloat64(gpuStat, gs) + break + } + } +} + +func getGPUInfo(ret *model.SystemInfo) (*model.SystemInfo, error) { + gpuModels, err := gpu.GetGPUModel() + if len(gpuModels) > 0 { + if err != nil { + hostDataFetchAttempts["GPU"]++ + return ret, fmt.Errorf("no gpu") + } else { + hostDataFetchAttempts["GPU"] = 0 + } + ret.GpuModel = gpuModels[0] + var wg sync.WaitGroup + wg.Add(1) + go updateGPUStat(&gpuStat, &wg) + wg.Wait() // 等待 updateGPUStat 完成 + ret.GpuStats = fmt.Sprintf("%f", math.Float64frombits(gpuStat)) + return ret, nil + } else { + hostDataFetchAttempts["GPU"]++ + return ret, fmt.Errorf("no gpu") + } +} diff --git a/system/gpu/gpu.go b/system/gpu/gpu.go new file mode 100644 index 0000000..c1d9355 --- /dev/null +++ b/system/gpu/gpu.go @@ -0,0 +1,27 @@ +//go:build !darwin +// +build !darwin + +package gpu + +import ( + "errors" + + "github.com/jaypipes/ghw" +) + +func GetGPUModel() ([]string, error) { + var gpuModel []string + gi, err := ghw.GPU(ghw.WithDisableWarnings()) + if err != nil { + return nil, err + } + + for _, card := range gi.GraphicsCards { + if card.DeviceInfo == nil { + return nil, errors.New("Cannot find device info") + } + gpuModel = append(gpuModel, card.DeviceInfo.Product.Name) + } + + return gpuModel, nil +} diff --git a/system/gpu/gpu_darwin.go b/system/gpu/gpu_darwin.go new file mode 100644 index 0000000..2664f8d --- /dev/null +++ b/system/gpu/gpu_darwin.go @@ -0,0 +1,53 @@ +//go:build darwin && !cgo + +package gpu + +import ( + "os/exec" + "regexp" + "strings" +) + +func extractGPUInfo(cmd *exec.Cmd) ([]string, error) { + gi, err := cmd.CombinedOutput() + if err != nil { + return nil, err + } + + re := regexp.MustCompile(`"model"\s*=\s*["<]?"([^">]+)"[">]?`) + matches := re.FindAllSubmatch(gi, -1) + var modelNames []string + for _, match := range matches { + if len(match) > 1 { + modelNames = append(modelNames, string(match[1])) + } + } + return modelNames, nil +} + +func GetGPUModel() ([]string, error) { + vendorNames := []string{ + "AMD", "Intel", "Nvidia", "Apple", + } + + ioreg := exec.Command("ioreg", "-rd1", "-c", "IOAccelerator") + gi, err := extractGPUInfo(ioreg) + if err != nil || len(gi) == 0 { + ioreg = exec.Command("ioreg", "-rd1", "-c", "IOPCIDevice") + gi, err = extractGPUInfo(ioreg) + if err != nil { + return nil, err + } + } + + var gpuModel []string + for _, model := range gi { + for _, vendor := range vendorNames { + if strings.Contains(model, vendor) { + gpuModel = append(gpuModel, model) + break + } + } + } + return gpuModel, nil +} diff --git a/system/gpu/gpu_darwin_cgo.go b/system/gpu/gpu_darwin_cgo.go new file mode 100644 index 0000000..1c4cc61 --- /dev/null +++ b/system/gpu/gpu_darwin_cgo.go @@ -0,0 +1,65 @@ +//go:build darwin && cgo + +package gpu + +// #cgo LDFLAGS: -framework IOKit -framework CoreFoundation +// #include "stat/gpu_darwin.h" +import "C" +import ( + "errors" + "strings" + "unsafe" +) + +func GoStrings(argc C.int, argv **C.char) []string { + length := int(argc) + tmpslice := unsafe.Slice(argv, length) + gostrings := make([]string, length) + for i, s := range tmpslice { + gostrings[i] = C.GoString(s) + } + return gostrings +} + +func extractGPUInfo(key *C.char) ([]string, error) { + devices := C.find_devices(key) + if devices != nil { + defer C.free(unsafe.Pointer(devices)) + length := 0 + for { + device := *(**C.char)(unsafe.Pointer(uintptr(unsafe.Pointer(devices)) + uintptr(length)*unsafe.Sizeof(*devices))) + if device == nil { + break + } + length++ + } + gpu := GoStrings(C.int(length), devices) + return gpu, nil + } + return nil, errors.New("cannot find key") +} + +func GetGPUModel() ([]string, error) { + vendorNames := []string{ + "AMD", "Intel", "Nvidia", "Apple", + } + + key := C.CString("model") + defer C.free(unsafe.Pointer(key)) + + gi, err := extractGPUInfo(key) + if err != nil { + return nil, err + } + + var gpuModel []string + for _, model := range gi { + for _, vendor := range vendorNames { + if strings.Contains(model, vendor) { + gpuModel = append(gpuModel, model) + break + } + } + } + return gpuModel, nil +} \ No newline at end of file diff --git a/system/gpu/stat/amd_rocm_smi.go b/system/gpu/stat/amd_rocm_smi.go new file mode 100644 index 0000000..3111f11 --- /dev/null +++ b/system/gpu/stat/amd_rocm_smi.go @@ -0,0 +1,66 @@ +package stat + +// Modified from https://github.com/influxdata/telegraf/blob/master/plugins/inputs/amd_rocm_smi/amd_rocm_smi.go +// Original License: MIT + +import ( + "errors" + "os" + "os/exec" + "strconv" + jsoniter "github.com/json-iterator/go" +) + +type ROCmSMI struct { + BinPath string +} + +func (rsmi *ROCmSMI) Gather() ([]float64, error) { + data := rsmi.pollROCmSMI() + + return gatherROCmSMI(data) +} + +func (rsmi *ROCmSMI) Start() error { + if _, err := os.Stat(rsmi.BinPath); os.IsNotExist(err) { + binPath, err := exec.LookPath("rocm-smi") + if err != nil { + return errors.New("Didn't find the adequate tool to query GPU utilization") + } + rsmi.BinPath = binPath + } + return nil +} + +func (rsmi *ROCmSMI) pollROCmSMI() []byte { + cmd := exec.Command(rsmi.BinPath, + "-u", + "--json", + ) + gs, err := cmd.CombinedOutput() + if err != nil { + return nil + } + return gs +} + +func gatherROCmSMI(ret []byte) ([]float64, error) { + var gpus map[string]GPU + var percentage []float64 + Json := jsoniter.ConfigCompatibleWithStandardLibrary + err := Json.Unmarshal(ret, &gpus) + if err != nil { + return nil, err + } + + for _, gpu := range gpus { + gp, _ := strconv.ParseFloat(gpu.GpuUsePercentage, 64) + percentage = append(percentage, gp) + } + + return percentage, nil +} + +type GPU struct { + GpuUsePercentage string `json:"GPU use (%)"` +} \ No newline at end of file diff --git a/system/gpu/stat/gpu_darwin.c b/system/gpu/stat/gpu_darwin.c new file mode 100644 index 0000000..18eba50 --- /dev/null +++ b/system/gpu/stat/gpu_darwin.c @@ -0,0 +1,144 @@ +#include "gpu_darwin.h" +#include +#include +#include +#include + +#define IOSERVICE_GPU "IOAccelerator" +#define IOSERVICE_PCI "IOPCIDevice" + +void *find_properties(io_registry_entry_t service, int depth, CFStringRef key, + CFStringRef dict_key) { + CFTypeRef properties = IORegistryEntrySearchCFProperty( + service, kIOServicePlane, key, kCFAllocatorDefault, + kIORegistryIterateRecursively); + + if (properties) { + if (CFGetTypeID(properties) == CFStringGetTypeID()) { + CFStringRef cfStr = (CFStringRef)properties; + char buffer[1024]; + CFStringGetCString(cfStr, buffer, sizeof(buffer), kCFStringEncodingUTF8); + CFRelease(properties); + return strdup(buffer); + } else if (CFGetTypeID(properties) == CFDictionaryGetTypeID()) { + CFDictionaryRef cfDict = (CFDictionaryRef)properties; + CFNumberRef cfValue = (CFNumberRef)CFDictionaryGetValue(cfDict, dict_key); + if (cfValue == NULL) { + return NULL; + } + int value; + if (!CFNumberGetValue(cfValue, kCFNumberIntType, &value)) { + return NULL; + } + return (void *)(intptr_t)value; + } + } + + return NULL; +} + +char **find_devices(char *key) { + io_service_t io_reg_err; + io_iterator_t iterator; + int capacity = 10; + + char **cards = malloc(capacity * sizeof(char *)); + if (!cards) { + fprintf(stderr, "Memory allocation failed\n"); + return NULL; + } + + io_reg_err = IOServiceGetMatchingServices( + kIOMainPortDefault, IOServiceMatching(IOSERVICE_GPU), &iterator); + if (io_reg_err != KERN_SUCCESS) { + printf("Error getting GPU entry\n"); + return NULL; + } + + io_object_t service; + int index = 0; + while ((service = IOIteratorNext(iterator)) != MACH_PORT_NULL) { + CFStringRef cfStr = CFStringCreateWithCString(kCFAllocatorDefault, key, + kCFStringEncodingUTF8); + char *result = find_properties(service, 0, cfStr, CFSTR("")); + CFRelease(cfStr); + IOObjectRelease(service); + + if (result != NULL) { + if (index >= capacity) { + capacity += 1; + char **new_cards = (char **)realloc(cards, capacity * sizeof(char *)); + if (!new_cards) { + fprintf(stderr, "Memory reallocation failed\n"); + for (int i = 0; i < index; i++) { + free(cards[i]); + } + free(cards); + free(result); + return NULL; + } + cards = new_cards; + } + cards[index] = result; + index++; + } + + if (result == NULL && strcmp(key, "model") == 0) { + IOObjectRelease(iterator); + + io_reg_err = IOServiceGetMatchingServices( + kIOMainPortDefault, IOServiceMatching(IOSERVICE_PCI), &iterator); + if (io_reg_err != KERN_SUCCESS) { + printf("Error getting PCI entry\n"); + return NULL; + } + } + } + IOObjectRelease(iterator); + + char **result_cards = (char **)realloc(cards, sizeof(char *) * (index + 1)); + if (!result_cards) { + fprintf(stderr, "Memory reallocation failed\n"); + for (int i = 0; i < index; i++) { + free(cards[i]); + } + free(cards); + return NULL; + } + result_cards[index] = NULL; + + return result_cards; +} + +int find_utilization(char *key, char *dict_key) { + void *result_ptr; + io_service_t io_reg_err; + io_iterator_t iterator; + + io_reg_err = IOServiceGetMatchingServices( + kIOMainPortDefault, IOServiceMatching(IOSERVICE_GPU), &iterator); + if (io_reg_err != KERN_SUCCESS) { + printf("Error getting GPU entry\n"); + return 0; + } + + io_object_t service = IOIteratorNext(iterator); + if (service != MACH_PORT_NULL) { + CFStringRef cfStr = CFStringCreateWithCString(kCFAllocatorDefault, key, + kCFStringEncodingUTF8); + CFStringRef cfDictStr = CFStringCreateWithCString( + kCFAllocatorDefault, dict_key, kCFStringEncodingUTF8); + result_ptr = find_properties(service, 0, cfStr, cfDictStr); + CFRelease(cfStr); + CFRelease(cfDictStr); + } + + IOObjectRelease(service); + IOObjectRelease(iterator); + + if (result_ptr == NULL) { + return 0; + } + + return (int)(intptr_t)result_ptr; +} \ No newline at end of file diff --git a/system/gpu/stat/gpu_darwin.h b/system/gpu/stat/gpu_darwin.h new file mode 100644 index 0000000..8dc6d0a --- /dev/null +++ b/system/gpu/stat/gpu_darwin.h @@ -0,0 +1,15 @@ +#ifndef __SMC_H__ +#define __SMC_H__ 1 + +#include +#include + +#if (defined __MAC_OS_X_VERSION_MIN_REQUIRED) && (__MAC_OS_X_VERSION_MIN_REQUIRED < 120000) +#define kIOMainPortDefault kIOMasterPortDefault +#endif + +void *find_properties(io_registry_entry_t, int, CFStringRef, CFStringRef); +char **find_devices(char *); +int find_utilization(char *, char *); + +#endif \ No newline at end of file diff --git a/system/gpu/stat/nvidia_smi.go b/system/gpu/stat/nvidia_smi.go new file mode 100644 index 0000000..0e14a83 --- /dev/null +++ b/system/gpu/stat/nvidia_smi.go @@ -0,0 +1,85 @@ +package stat + +// Modified from https://github.com/influxdata/telegraf/blob/master/plugins/inputs/nvidia_smi/nvidia_smi.go +// Original License: MIT + +import ( + "encoding/xml" + "errors" + "os" + "os/exec" + "strconv" + "strings" +) + +type NvidiaSMI struct { + BinPath string +} + +func (smi *NvidiaSMI) Gather() ([]float64, error) { + data := smi.pollNvidiaSMI() + + return smi.parse(data) +} + +func (smi *NvidiaSMI) Start() error { + if _, err := os.Stat(smi.BinPath); os.IsNotExist(err) { + binPath, err := exec.LookPath("nvidia-smi") + if err != nil { + return errors.New("Didn't find the adequate tool to query GPU utilization") + } + smi.BinPath = binPath + } + return nil +} + +func (smi *NvidiaSMI) pollNvidiaSMI() []byte { + cmd := exec.Command(smi.BinPath, + "-q", + "-x", + ) + gs, err := cmd.CombinedOutput() + if err != nil { + return nil + } + return gs +} + +func (smi *NvidiaSMI) parse(data []byte) ([]float64, error) { + var s smistat + var percentage []float64 + + err := xml.Unmarshal(data, &s) + if err != nil { + return nil, err + } + + for _, gpu := range s.GPUs { + gp, _ := parsePercentage(gpu.Utilization.GpuUtil) + percentage = append(percentage, gp) + } + + return percentage, nil +} + +func parsePercentage(p string) (float64, error) { + per := strings.ReplaceAll(p, " ", "") + + t := strings.TrimSuffix(per, "%") + + value, err := strconv.ParseFloat(t, 64) + if err != nil { + return 0, err + } + + return value, nil +} + +type nGPU struct { + Utilization struct { + GpuUtil string `xml:"gpu_util"` + } `xml:"utilization"` +} +type smistat struct { + GPUs []nGPU `xml:"gpu"` +} diff --git a/system/gpu/stat/stat_darwin.go b/system/gpu/stat/stat_darwin.go new file mode 100644 index 0000000..a841f81 --- /dev/null +++ b/system/gpu/stat/stat_darwin.go @@ -0,0 +1,36 @@ +//go:build darwin && !cgo + +package stat + +import ( + "os/exec" + "regexp" + "strconv" +) + +func extractGPUStat(cmd *exec.Cmd) ([]float64, error) { + gs, err := cmd.CombinedOutput() + if err != nil { + return nil, err + } + + re := regexp.MustCompile(`"Device Utilization %"\s*=\s*(\d+)`) + matches := re.FindAllSubmatch(gs, -1) + var u []float64 + for _, match := range matches { + if len(match) > 1 { + p, _ := strconv.ParseFloat(string(match[1]), 64) + u = append(u, p) + } + } + return u, nil +} + +func GetGPUStat() (float64, error) { + ioreg := exec.Command("ioreg", "-rd1", "-c", "IOAccelerator") + gs, err := extractGPUStat(ioreg) + if err != nil || len(gs) == 0 { + return 0, err + } + return gs[0], nil +} diff --git a/system/gpu/stat/stat_darwin_cgo.go b/system/gpu/stat/stat_darwin_cgo.go new file mode 100644 index 0000000..19f00a1 --- /dev/null +++ b/system/gpu/stat/stat_darwin_cgo.go @@ -0,0 +1,25 @@ +//go:build darwin && cgo + +package stat + +// #cgo LDFLAGS: -framework IOKit -framework CoreFoundation +// #include "gpu_darwin.h" +import "C" +import ( + "unsafe" +) + +func extractGPUStat(key *C.char, dict_key *C.char) (int, error) { + utilization := C.find_utilization(key, dict_key) + return int(utilization), nil +} + +func GetGPUStat() (float64, error) { + key := C.CString("PerformanceStatistics") + dict_key := C.CString("Device Utilization %") + defer C.free(unsafe.Pointer(key)) + defer C.free(unsafe.Pointer(dict_key)) + + gs, _ := extractGPUStat(key, dict_key) + return float64(gs), nil +} diff --git a/system/gpu/stat/stat_freebsd.go b/system/gpu/stat/stat_freebsd.go new file mode 100644 index 0000000..dcd0da7 --- /dev/null +++ b/system/gpu/stat/stat_freebsd.go @@ -0,0 +1,7 @@ +//go:build freebsd + +package stat + +func GetGPUStat() (float64, error) { + return 0, nil +} diff --git a/system/gpu/stat/stat_linux.go b/system/gpu/stat/stat_linux.go new file mode 100644 index 0000000..f7274c8 --- /dev/null +++ b/system/gpu/stat/stat_linux.go @@ -0,0 +1,44 @@ +//go:build linux + +package stat + +func getNvidiaStat() ([]float64, error) { + smi := &NvidiaSMI{ + BinPath: "/usr/bin/nvidia-smi", + } + err1 := smi.Start() + if err1 != nil { + return nil, err1 + } + data, err2 := smi.Gather() + if err2 != nil { + return nil, err2 + } + return data, nil +} + +func getAMDStat() ([]float64, error) { + rsmi := &ROCmSMI{ + BinPath: "/opt/rocm/bin/rocm-smi", + } + err1 := rsmi.Start() + if err1 != nil { + return nil, err1 + } + data, err2 := rsmi.Gather() + if err2 != nil { + return nil, err2 + } + return data, nil +} + +func GetGPUStat() (float64, error) { + gs, err := getNvidiaStat() + if err != nil { + gs, err = getAMDStat() + } + if err != nil || len(gs) == 0 { + return 0, err + } + return gs[0], nil +} diff --git a/system/gpu/stat/stat_windows.go b/system/gpu/stat/stat_windows.go new file mode 100644 index 0000000..7cb566f --- /dev/null +++ b/system/gpu/stat/stat_windows.go @@ -0,0 +1,151 @@ +//go:build windows + +// Modified from https://github.com/shirou/gopsutil/blob/master/internal/common/common_windows.go +// Original License: BSD-3-Clause + +package stat + +import ( + "errors" + "fmt" + "time" + "unsafe" + + "golang.org/x/sys/windows" +) + +const ( + ERROR_SUCCESS = 0 + PDH_FMT_DOUBLE = 0x00000200 + PDH_MORE_DATA = 0x800007d2 + PDH_VAILD_DATA = 0x00000000 + PDH_NEW_DATA = 0x00000001 + PDH_NO_DATA = 0x800007d5 +) + +var ( + modPdh = windows.NewLazySystemDLL("pdh.dll") + + pdhOpenQuery = modPdh.NewProc("PdhOpenQuery") + pdhCollectQueryData = modPdh.NewProc("PdhCollectQueryData") + pdhGetFormattedCounterArrayW = modPdh.NewProc("PdhGetFormattedCounterArrayW") + pdhAddEnglishCounterW = modPdh.NewProc("PdhAddEnglishCounterW") +) + +type PDH_FMT_COUNTERVALUE_DOUBLE struct { + CStatus uint32 + DoubleValue float64 +} + +type PDH_FMT_COUNTERVALUE_ITEM_DOUBLE struct { + SzName *uint16 + FmtValue PDH_FMT_COUNTERVALUE_DOUBLE +} + +// https://github.com/influxdata/telegraf/blob/master/plugins/inputs/win_perf_counters/performance_query.go +func getCounterArrayValue(initialBufSize uint32, counter *win32PerformanceCounter) ([]float64, error) { + for buflen := initialBufSize; buflen <= 100*1024*1024; buflen *= 2 { + time.Sleep(10 * time.Millisecond) // GPU 查询必须设置间隔,否则数据不准 + s, _, err := pdhCollectQueryData.Call(uintptr(counter.Query)) + if s != 0 && err != nil { + if s == PDH_NO_DATA { + return nil, fmt.Errorf("%w: this counter has not data", err) + } + return nil, err + } + buf := make([]byte, buflen) + size := buflen + var itemCount uint32 + r, _, _ := pdhGetFormattedCounterArrayW.Call(uintptr(counter.Counter), PDH_FMT_DOUBLE, uintptr(unsafe.Pointer(&size)), uintptr(unsafe.Pointer(&itemCount)), uintptr(unsafe.Pointer(&buf[0]))) + if r == ERROR_SUCCESS { + items := (*[1 << 20]PDH_FMT_COUNTERVALUE_ITEM_DOUBLE)(unsafe.Pointer(&buf[0]))[:itemCount:itemCount] + values := make([]float64, 0, itemCount) + for _, item := range items { + if item.FmtValue.CStatus == PDH_VAILD_DATA || item.FmtValue.CStatus == PDH_NEW_DATA { + val := item.FmtValue.DoubleValue + values = append(values, val) + } + } + return values, nil + } + if r != PDH_MORE_DATA { + return nil, fmt.Errorf("pdhGetFormattedCounterArrayW failed with status 0x%X", r) + } + } + + return nil, errors.New("buffer limit reached") +} + +func createQuery() (windows.Handle, error) { + var query windows.Handle + r, _, err := pdhOpenQuery.Call(0, 0, uintptr(unsafe.Pointer(&query))) + if r != ERROR_SUCCESS { + return 0, fmt.Errorf("pdhOpenQuery failed with status 0x%X: %v", r, err) + } + return query, nil +} + +type win32PerformanceCounter struct { + PostName string + CounterName string + Query windows.Handle + Counter windows.Handle +} + +func newWin32PerformanceCounter(postName, counterName string) (*win32PerformanceCounter, error) { + query, err := createQuery() + if err != nil { + return nil, err + } + counter := win32PerformanceCounter{ + Query: query, + PostName: postName, + CounterName: counterName, + } + r, _, err := pdhAddEnglishCounterW.Call( + uintptr(counter.Query), + uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(counter.CounterName))), + 0, + uintptr(unsafe.Pointer(&counter.Counter)), + ) + if r != ERROR_SUCCESS { + return nil, fmt.Errorf("pdhAddEnglishCounterW failed with status 0x%X: %v", r, err) + } + return &counter, nil +} + +func getValue(initialBufSize uint32, counter *win32PerformanceCounter) ([]float64, error) { + s, _, err := pdhCollectQueryData.Call(uintptr(counter.Query)) + if s != 0 && err != nil { + if s == PDH_NO_DATA { + return nil, fmt.Errorf("%w: this counter has not data", err) + } + return nil, err + } + + return getCounterArrayValue(initialBufSize, counter) +} + +func GetGPUStat() (float64, error) { + counter, err := newWin32PerformanceCounter("gpu_utilization", "\\GPU Engine(*engtype_3D)\\Utilization Percentage") + if err != nil { + return 0, err + } + values, err := getValue(1024, counter) + if err != nil { + return 0, err + } + tot := sumArray(values) + if tot > 100 { + tot = 100 + } + return tot, nil +} + +func sumArray(arr []float64) float64 { + var sum float64 + for _, value := range arr { + sum += value + } + return sum +} diff --git a/system/system.go b/system/system.go index e019ae8..9ef3264 100644 --- a/system/system.go +++ b/system/system.go @@ -6,6 +6,7 @@ import ( "github.com/oneclickvirt/basics/model" "github.com/oneclickvirt/basics/system/utils" + . "github.com/oneclickvirt/defaultset" ) var ( @@ -18,16 +19,35 @@ var ( // GetSystemInfo 获取主机硬件信息 func GetSystemInfo() *model.SystemInfo { + if model.EnableLoger { + InitLogger() + defer Logger.Sync() + } var ret = &model.SystemInfo{} + var err error if runtime.GOOS == "darwin" { getMacOSInfo() } // 系统信息查询 - cpuType, ret.Uptime, ret.Platform, ret.Kernel, ret.Arch, ret.VmType, ret.NatType, ret.TimeZone, _ = getHostInfo() + cpuType, ret.Uptime, ret.Platform, ret.Kernel, ret.Arch, ret.VmType, ret.NatType, ret.TimeZone, err = getHostInfo() + if model.EnableLoger { + Logger.Info(err.Error()) + } // CPU信息查询 - ret, _ = getCpuInfo(ret, cpuType) + ret, err = getCpuInfo(ret, cpuType) + if model.EnableLoger { + Logger.Info(err.Error()) + } + // GPU信息查询 + ret, err = getGPUInfo(ret) + if model.EnableLoger { + Logger.Info(err.Error()) + } // 硬盘信息查询 - ret.DiskTotal, ret.DiskUsage, ret.Percentage, ret.BootPath, _ = getDiskInfo() + ret.DiskTotal, ret.DiskUsage, ret.Percentage, ret.BootPath, err = getDiskInfo() + if model.EnableLoger { + Logger.Info(err.Error()) + } // 内存信息查询 ret.MemoryTotal, ret.MemoryUsage, ret.SwapTotal, ret.SwapUsage, ret.VirtioBalloon, ret.KSM = getMemoryInfo() // 获取负载信息 @@ -52,6 +72,12 @@ func CheckSystemInfo(language string) string { if ret.CpuCache != "" { res += " CPU Cache : " + ret.CpuCache + "\n" } + if ret.GpuModel != "" { + res += " GPU Model : " + ret.GpuModel + "\n" + if ret.GpuStats != "" && ret.GpuStats != "0" { + res += " GPU Stats : " + ret.GpuStats + "\n" + } + } if runtime.GOOS != "windows" && runtime.GOOS != "macos" { res += " AES-NI : " + ret.CpuAesNi + "\n" } @@ -95,6 +121,12 @@ func CheckSystemInfo(language string) string { if ret.CpuCache != "" { res += " CPU 缓存 : " + ret.CpuCache + "\n" } + if ret.GpuModel != "" { + res += " GPU 型号 : " + ret.GpuModel + "\n" + if ret.GpuStats != "" && ret.GpuStats != "0" { + res += " GPU 状态 : " + ret.GpuStats + "\n" + } + } if runtime.GOOS != "windows" && runtime.GOOS != "macos" { res += " AES-NI : " + ret.CpuAesNi + "\n" }