diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..657b3ae --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,13 @@ +# Security Policy + +## Supported Versions + +Security updates are applied only to the latest release. + +## Reporting a Vulnerability + +If you have discovered a security vulnerability in this project, please report it privately. **Do not disclose it as a public issue.** This gives us time to work with you to fix the issue before public exposure, reducing the chance that the exploit will be used before a patch is released. + +Please disclose it at [Security Advisories](https://github.com/shirou/gopsutil/security/advisories/new). + +This project is maintained by a team of volunteers on a reasonable-effort basis. As such, vulnerability reports will be investigated and fixed or disclosed as soon as possible. diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml index 48a5801..ce255f9 100644 --- a/.github/workflows/build_test.yml +++ b/.github/workflows/build_test.yml @@ -19,20 +19,20 @@ jobs: fail-fast: false matrix: go-version: ${{fromJson(needs.go-versions.outputs.versions)}} - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Install Go - uses: actions/setup-go@v3 + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: go-version: ${{ matrix.go-version }} - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - id: cache-paths run: | echo "::set-output name=cache::$(go env GOCACHE)" echo "::set-output name=mod-cache::$(go env GOMODCACHE)" - name: Cache go modules - uses: actions/cache@v3 + uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1 with: path: | ${{ steps.cache-paths.outputs.cache }} diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 218b89e..5626060 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -12,6 +12,6 @@ jobs: pull-requests: write # for actions/labeler to add labels to PRs runs-on: ubuntu-latest steps: - - uses: actions/labeler@v4 + - uses: actions/labeler@ac9175f8a1f3625fd0d4fb234536d26811351594 # v4.3.0 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3251245..5a9ebca 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,13 +16,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup go - uses: actions/setup-go@v3 + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: go-version: 1.17 - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - name: Setup golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@3a919529898de77ec3da873e3063ca4b10e7f5cc # v3.7.0 with: args: --verbose version: latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6217ef9..efd3bae 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,6 +10,6 @@ jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - name: Release run: make release diff --git a/.github/workflows/sbom_generator.yml b/.github/workflows/sbom_generator.yml new file mode 100644 index 0000000..97565c1 --- /dev/null +++ b/.github/workflows/sbom_generator.yml @@ -0,0 +1,25 @@ +name: SBOM Generator + +on: + push: + branches: [ "master" ] + + workflow_dispatch: + +permissions: read-all + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + + - uses: advanced-security/sbom-generator-action@375dee8e6144d9fd0ec1f5667b4f6fb4faacefed # v0.0.1 + id: sbom + env: + GITHUB_TOKEN: ${{ github.token }} + - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + with: + path: ${{steps.sbom.outputs.fileName }} + name: "SBOM" diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml index 04bb575..b700ca8 100644 --- a/.github/workflows/shellcheck.yml +++ b/.github/workflows/shellcheck.yml @@ -8,6 +8,6 @@ jobs: name: Shellcheck runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - name: Run ShellCheck - uses: ludeeus/action-shellcheck@master \ No newline at end of file + uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # v2.0.0 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dd63787..dacda8b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,21 +19,21 @@ jobs: fail-fast: false matrix: go-version: ${{fromJson(needs.go-versions.outputs.versions)}} - os: [ubuntu-20.04, ubuntu-18.04, windows-2019, macos-11] + os: [ubuntu-22.04, ubuntu-20.04, windows-2022, windows-2019, macos-11, macos-12] runs-on: ${{ matrix.os }} steps: - name: Install Go - uses: actions/setup-go@v3 + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: go-version: ${{ matrix.go-version }} - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - id: go-env run: | echo "::set-output name=cache::$(go env GOCACHE)" echo "::set-output name=mod-cache::$(go env GOMODCACHE)" - name: Cache go modules - uses: actions/cache@v3 + uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1 with: path: | ${{ steps.go-env.outputs.cache }} diff --git a/.golangci.yml b/.golangci.yml index b175f43..4d163db 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -3,13 +3,19 @@ issues: exclude-rules: - linters: - gosec - text: "G204" + text: "G204" - linters: - revive - text: "var-naming" + text: "var-naming" - linters: - revive - text: "exported" + text: "exported" + - linters: + - revive + text: "empty-block" + - linters: + - revive + text: "unused-parameter" linters: enable: - asciicheck @@ -40,3 +46,10 @@ linters: - structcheck - unused - varcheck + +linters-settings: + gci: + sections: + - standard + - default + - prefix(github.com/shirou) diff --git a/Makefile b/Makefile index 9f6639e..3f5cd84 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,9 @@ build_test: ## test only buildable GOOS=linux GOARCH=386 go test ./... | $(BUILD_FAIL_PATTERN) GOOS=linux GOARCH=arm go test ./... | $(BUILD_FAIL_PATTERN) GOOS=linux GOARCH=arm64 go test ./... | $(BUILD_FAIL_PATTERN) + GOOS=linux GOARCH=loong64 go test ./... | $(BUILD_FAIL_PATTERN) GOOS=linux GOARCH=riscv64 go test ./... | $(BUILD_FAIL_PATTERN) + GOOS=linux GOARCH=s390x go test ./... | $(BUILD_FAIL_PATTERN) GOOS=freebsd GOARCH=amd64 go test ./... | $(BUILD_FAIL_PATTERN) GOOS=freebsd GOARCH=386 go test ./... | $(BUILD_FAIL_PATTERN) GOOS=freebsd GOARCH=arm go test ./... | $(BUILD_FAIL_PATTERN) @@ -51,11 +53,13 @@ vet: GOOS=linux GOARCH=amd64 go vet ./... GOOS=linux GOARCH=arm64 go vet ./... GOOS=linux GOARCH=arm go vet ./... + GOOS=linux GOARCH=loong64 go vet ./... GOOS=linux GOARCH=mips64 go vet ./... GOOS=linux GOARCH=mips64le go vet ./... GOOS=linux GOARCH=mips go vet ./... GOOS=linux GOARCH=mipsle go vet ./... GOOS=linux GOARCH=ppc64le go vet ./... + GOOS=linux GOARCH=ppc64 go vet ./... GOOS=linux GOARCH=riscv64 go vet ./... GOOS=linux GOARCH=s390x go vet ./... diff --git a/README.md b/README.md index 840c353..78751d7 100644 --- a/README.md +++ b/README.md @@ -90,9 +90,25 @@ environment variable. You can set an alternative location to `/dev` by setting the `HOST_DEV` environment variable. +You can set an alternative location to `/` by setting the `HOST_ROOT` +environment variable. + You can set an alternative location to `/proc/N/mountinfo` by setting the `HOST_PROC_MOUNTINFO` environment variable. +### Adding settings using `context` (from v3.23.6) + +As of v3.23.6, it is now possible to pass a path location using `context`: import `"github.com/shirou/gopsutil/v3/common"` and pass a context with `common.EnvMap` set to `common.EnvKey`, and the location will be used within each function. + +``` + ctx := context.WithValue(context.Background(), + common.EnvKey, common.EnvMap{common.HostProcEnvKey: "/myproc"}, + ) + v, err := mem.VirtualMemoryWithContext(ctx) +``` + +First priority is given to the value set in `context`, then the value from the environment variable, and finally the default location. + ## Documentation See https://pkg.go.dev/github.com/shirou/gopsutil/v3 or https://godocs.io/github.com/shirou/gopsutil/v3 @@ -151,7 +167,7 @@ will provide useful information. - system wide stats on netfilter conntrack module - sourced from /proc/sys/net/netfilter/nf_conntrack_count -Some code is ported from Ohai. many thanks. +Some code is ported from Ohai. Many thanks. ## Current Status @@ -216,9 +232,9 @@ Some code is ported from Ohai. many thanks. |rlimit |x | | | | | |num\_handlers | | | | | | |threads |x | | | | | -|cpu\_percent |x | |x |x | | +|cpu\_percent |x | |x |x |x | |cpu\_affinity | | | | | | -|memory\_percent | | | | | | +|memory\_percent |x | | | |x | |parent |x | |x |x |x | |children |x |x |x |x |x | |connections |x | |x |x | | diff --git a/common/env.go b/common/env.go new file mode 100644 index 0000000..4b5f498 --- /dev/null +++ b/common/env.go @@ -0,0 +1,23 @@ +package common + +type EnvKeyType string + +// EnvKey is a context key that can be used to set programmatically the environment +// gopsutil relies on to perform calls against the OS. +// Example of use: +// +// ctx := context.WithValue(context.Background(), common.EnvKey, EnvMap{common.HostProcEnvKey: "/myproc"}) +// avg, err := load.AvgWithContext(ctx) +var EnvKey = EnvKeyType("env") + +const ( + HostProcEnvKey EnvKeyType = "HOST_PROC" + HostSysEnvKey EnvKeyType = "HOST_SYS" + HostEtcEnvKey EnvKeyType = "HOST_ETC" + HostVarEnvKey EnvKeyType = "HOST_VAR" + HostRunEnvKey EnvKeyType = "HOST_RUN" + HostDevEnvKey EnvKeyType = "HOST_DEV" + HostRootEnvKey EnvKeyType = "HOST_ROOT" +) + +type EnvMap map[EnvKeyType]string diff --git a/cpu/cpu_aix_nocgo.go b/cpu/cpu_aix_nocgo.go index d158000..1a29153 100644 --- a/cpu/cpu_aix_nocgo.go +++ b/cpu/cpu_aix_nocgo.go @@ -6,8 +6,8 @@ package cpu import ( "context" "regexp" - "strings" "strconv" + "strings" "github.com/shirou/gopsutil/v3/internal/common" ) @@ -28,19 +28,19 @@ func TimesWithContext(ctx context.Context, percpu bool) ([]TimesStat, error) { } ret := TimesStat{CPU: "cpu-total"} - h := whiteSpaces.Split(lines[len(lines)-3], -1) // headers - v := whiteSpaces.Split(lines[len(lines)-2], -1) // values + h := whiteSpaces.Split(lines[len(lines)-3], -1) // headers + v := whiteSpaces.Split(lines[len(lines)-2], -1) // values for i, header := range h { if t, err := strconv.ParseFloat(v[i], 64); err == nil { switch header { - case `%usr`: - ret.User = t - case `%sys`: - ret.System = t - case `%wio`: - ret.Iowait = t - case `%idle`: - ret.Idle = t + case `%usr`: + ret.User = t + case `%sys`: + ret.System = t + case `%wio`: + ret.Iowait = t + case `%idle`: + ret.Idle = t } } } diff --git a/cpu/cpu_darwin.go b/cpu/cpu_darwin.go index 7acb258..41f395e 100644 --- a/cpu/cpu_darwin.go +++ b/cpu/cpu_darwin.go @@ -8,6 +8,7 @@ import ( "strconv" "strings" + "github.com/shoenig/go-m1cpu" "github.com/tklauser/go-sysconf" "golang.org/x/sys/unix" ) @@ -85,11 +86,15 @@ func InfoWithContext(ctx context.Context) ([]InfoStat, error) { c.CacheSize = int32(cacheSize) c.VendorID, _ = unix.Sysctl("machdep.cpu.vendor") - // Use the rated frequency of the CPU. This is a static value and does not - // account for low power or Turbo Boost modes. - cpuFrequency, err := unix.SysctlUint64("hw.cpufrequency") - if err == nil { - c.Mhz = float64(cpuFrequency) / 1000000.0 + if m1cpu.IsAppleSilicon() { + c.Mhz = float64(m1cpu.PCoreHz() / 1_000_000) + } else { + // Use the rated frequency of the CPU. This is a static value and does not + // account for low power or Turbo Boost modes. + cpuFrequency, err := unix.SysctlUint64("hw.cpufrequency") + if err == nil { + c.Mhz = float64(cpuFrequency) / 1000000.0 + } } return append(ret, c), nil diff --git a/cpu/cpu_darwin_test.go b/cpu/cpu_darwin_test.go new file mode 100644 index 0000000..57b3d66 --- /dev/null +++ b/cpu/cpu_darwin_test.go @@ -0,0 +1,33 @@ +//go:build darwin +// +build darwin + +package cpu + +import ( + "testing" + + "github.com/shoenig/go-m1cpu" +) + +func Test_CpuInfo_AppleSilicon(t *testing.T) { + if !m1cpu.IsAppleSilicon() { + t.Skip("wrong cpu type") + } + + v, err := Info() + if err != nil { + t.Errorf("cpu info should be implemented on darwin systems") + } + + for _, vv := range v { + if vv.ModelName == "" { + t.Errorf("could not get CPU info: %v", vv) + } + if vv.Mhz <= 0 { + t.Errorf("could not get frequency of: %s", vv.ModelName) + } + if vv.Mhz > 6000 { + t.Errorf("cpu frequency is absurdly high value: %f MHz", vv.Mhz) + } + } +} diff --git a/cpu/cpu_linux.go b/cpu/cpu_linux.go index 4f26230..b5a20e3 100644 --- a/cpu/cpu_linux.go +++ b/cpu/cpu_linux.go @@ -11,12 +11,78 @@ import ( "strconv" "strings" - "github.com/shirou/gopsutil/v3/internal/common" "github.com/tklauser/go-sysconf" + + "github.com/shirou/gopsutil/v3/internal/common" ) var ClocksPerSec = float64(100) +var armModelToModelName = map[uint64]string{ + 0x810: "ARM810", + 0x920: "ARM920", + 0x922: "ARM922", + 0x926: "ARM926", + 0x940: "ARM940", + 0x946: "ARM946", + 0x966: "ARM966", + 0xa20: "ARM1020", + 0xa22: "ARM1022", + 0xa26: "ARM1026", + 0xb02: "ARM11 MPCore", + 0xb36: "ARM1136", + 0xb56: "ARM1156", + 0xb76: "ARM1176", + 0xc05: "Cortex-A5", + 0xc07: "Cortex-A7", + 0xc08: "Cortex-A8", + 0xc09: "Cortex-A9", + 0xc0d: "Cortex-A17", + 0xc0f: "Cortex-A15", + 0xc0e: "Cortex-A17", + 0xc14: "Cortex-R4", + 0xc15: "Cortex-R5", + 0xc17: "Cortex-R7", + 0xc18: "Cortex-R8", + 0xc20: "Cortex-M0", + 0xc21: "Cortex-M1", + 0xc23: "Cortex-M3", + 0xc24: "Cortex-M4", + 0xc27: "Cortex-M7", + 0xc60: "Cortex-M0+", + 0xd01: "Cortex-A32", + 0xd02: "Cortex-A34", + 0xd03: "Cortex-A53", + 0xd04: "Cortex-A35", + 0xd05: "Cortex-A55", + 0xd06: "Cortex-A65", + 0xd07: "Cortex-A57", + 0xd08: "Cortex-A72", + 0xd09: "Cortex-A73", + 0xd0a: "Cortex-A75", + 0xd0b: "Cortex-A76", + 0xd0c: "Neoverse-N1", + 0xd0d: "Cortex-A77", + 0xd0e: "Cortex-A76AE", + 0xd13: "Cortex-R52", + 0xd20: "Cortex-M23", + 0xd21: "Cortex-M33", + 0xd40: "Neoverse-V1", + 0xd41: "Cortex-A78", + 0xd42: "Cortex-A78AE", + 0xd43: "Cortex-A65AE", + 0xd44: "Cortex-X1", + 0xd46: "Cortex-A510", + 0xd47: "Cortex-A710", + 0xd48: "Cortex-X2", + 0xd49: "Neoverse-N2", + 0xd4a: "Neoverse-E1", + 0xd4b: "Cortex-A78C", + 0xd4c: "Cortex-X1C", + 0xd4d: "Cortex-A715", + 0xd4e: "Cortex-X3", +} + func init() { clkTck, err := sysconf.Sysconf(sysconf.SC_CLK_TCK) // ignore errors @@ -30,7 +96,7 @@ func Times(percpu bool) ([]TimesStat, error) { } func TimesWithContext(ctx context.Context, percpu bool) ([]TimesStat, error) { - filename := common.HostProc("stat") + filename := common.HostProcWithContext(ctx, "stat") lines := []string{} if percpu { statlines, err := common.ReadLines(filename) @@ -60,17 +126,17 @@ func TimesWithContext(ctx context.Context, percpu bool) ([]TimesStat, error) { return ret, nil } -func sysCPUPath(cpu int32, relPath string) string { - return common.HostSys(fmt.Sprintf("devices/system/cpu/cpu%d", cpu), relPath) +func sysCPUPath(ctx context.Context, cpu int32, relPath string) string { + return common.HostSysWithContext(ctx, fmt.Sprintf("devices/system/cpu/cpu%d", cpu), relPath) } -func finishCPUInfo(c *InfoStat) { +func finishCPUInfo(ctx context.Context, c *InfoStat) { var lines []string var err error var value float64 if len(c.CoreID) == 0 { - lines, err = common.ReadLines(sysCPUPath(c.CPU, "topology/core_id")) + lines, err = common.ReadLines(sysCPUPath(ctx, c.CPU, "topology/core_id")) if err == nil { c.CoreID = lines[0] } @@ -79,7 +145,7 @@ func finishCPUInfo(c *InfoStat) { // override the value of c.Mhz with cpufreq/cpuinfo_max_freq regardless // of the value from /proc/cpuinfo because we want to report the maximum // clock-speed of the CPU for c.Mhz, matching the behaviour of Windows - lines, err = common.ReadLines(sysCPUPath(c.CPU, "cpufreq/cpuinfo_max_freq")) + lines, err = common.ReadLines(sysCPUPath(ctx, c.CPU, "cpufreq/cpuinfo_max_freq")) // if we encounter errors below such as there are no cpuinfo_max_freq file, // we just ignore. so let Mhz is 0. if err != nil || len(lines) == 0 { @@ -107,7 +173,7 @@ func Info() ([]InfoStat, error) { } func InfoWithContext(ctx context.Context) ([]InfoStat, error) { - filename := common.HostProc("cpuinfo") + filename := common.HostProcWithContext(ctx, "cpuinfo") lines, _ := common.ReadLines(filename) var ret []InfoStat @@ -125,9 +191,9 @@ func InfoWithContext(ctx context.Context) ([]InfoStat, error) { switch key { case "Processor": processorName = value - case "processor": + case "processor", "cpu number": if c.CPU >= 0 { - finishCPUInfo(&c) + finishCPUInfo(ctx, &c) ret = append(ret, c) } c = InfoStat{Cores: 1, ModelName: processorName} @@ -138,6 +204,9 @@ func InfoWithContext(ctx context.Context) ([]InfoStat, error) { c.CPU = int32(t) case "vendorId", "vendor_id": c.VendorID = value + if strings.Contains(value, "S390") { + processorName = "S390" + } case "CPU implementer": if v, err := strconv.ParseUint(value, 0, 8); err == nil { switch v { @@ -177,10 +246,20 @@ func InfoWithContext(ctx context.Context) ([]InfoStat, error) { c.Family = value case "model", "CPU part": c.Model = value - case "model name", "cpu": + // if CPU is arm based, model name is found via model number. refer to: arch/arm64/kernel/cpuinfo.c + if c.VendorID == "ARM" { + if v, err := strconv.ParseUint(c.Model, 0, 16); err == nil { + modelName, exist := armModelToModelName[v] + if exist { + c.ModelName = modelName + } else { + c.ModelName = "Undefined" + } + } + } + case "Model Name", "model name", "cpu": c.ModelName = value - if strings.Contains(value, "POWER8") || - strings.Contains(value, "POWER7") { + if strings.Contains(value, "POWER") { c.Model = strings.Split(value, " ")[0] c.Family = "POWER" c.VendorID = "IBM" @@ -197,7 +276,7 @@ func InfoWithContext(ctx context.Context) ([]InfoStat, error) { return ret, err } c.Stepping = int32(t) - case "cpu MHz", "clock": + case "cpu MHz", "clock", "cpu MHz dynamic": // treat this as the fallback value, thus we ignore error if t, err := strconv.ParseFloat(strings.Replace(value, "MHz", "", 1), 64); err == nil { c.Mhz = t @@ -221,7 +300,7 @@ func InfoWithContext(ctx context.Context) ([]InfoStat, error) { } } if c.CPU >= 0 { - finishCPUInfo(&c) + finishCPUInfo(ctx, &c) ret = append(ret, c) } return ret, nil @@ -310,7 +389,7 @@ func CountsWithContext(ctx context.Context, logical bool) (int, error) { if logical { ret := 0 // https://github.com/giampaolo/psutil/blob/d01a9eaa35a8aadf6c519839e987a49d8be2d891/psutil/_pslinux.py#L599 - procCpuinfo := common.HostProc("cpuinfo") + procCpuinfo := common.HostProcWithContext(ctx, "cpuinfo") lines, err := common.ReadLines(procCpuinfo) if err == nil { for _, line := range lines { @@ -324,7 +403,7 @@ func CountsWithContext(ctx context.Context, logical bool) (int, error) { } } if ret == 0 { - procStat := common.HostProc("stat") + procStat := common.HostProcWithContext(ctx, "stat") lines, err = common.ReadLines(procStat) if err != nil { return 0, err @@ -345,7 +424,7 @@ func CountsWithContext(ctx context.Context, logical bool) (int, error) { // https://github.com/giampaolo/psutil/pull/1727#issuecomment-707624964 // https://lkml.org/lkml/2019/2/26/41 for _, glob := range []string{"devices/system/cpu/cpu[0-9]*/topology/core_cpus_list", "devices/system/cpu/cpu[0-9]*/topology/thread_siblings_list"} { - if files, err := filepath.Glob(common.HostSys(glob)); err == nil { + if files, err := filepath.Glob(common.HostSysWithContext(ctx, glob)); err == nil { for _, file := range files { lines, err := common.ReadLines(file) if err != nil || len(lines) != 1 { @@ -360,7 +439,7 @@ func CountsWithContext(ctx context.Context, logical bool) (int, error) { } } // https://github.com/giampaolo/psutil/blob/122174a10b75c9beebe15f6c07dcf3afbe3b120d/psutil/_pslinux.py#L631-L652 - filename := common.HostProc("cpuinfo") + filename := common.HostProcWithContext(ctx, "cpuinfo") lines, err := common.ReadLines(filename) if err != nil { return 0, err diff --git a/cpu/cpu_linux_test.go b/cpu/cpu_linux_test.go index f66d6b9..e06f305 100644 --- a/cpu/cpu_linux_test.go +++ b/cpu/cpu_linux_test.go @@ -2,7 +2,6 @@ package cpu import ( "errors" - "os" "os/exec" "strconv" "strings" @@ -10,8 +9,7 @@ import ( ) func TestTimesEmpty(t *testing.T) { - orig := os.Getenv("HOST_PROC") - os.Setenv("HOST_PROC", "testdata/linux/times_empty") + t.Setenv("HOST_PROC", "testdata/linux/times_empty") _, err := Times(true) if err != nil { t.Error("Times(true) failed") @@ -20,12 +18,10 @@ func TestTimesEmpty(t *testing.T) { if err != nil { t.Error("Times(false) failed") } - os.Setenv("HOST_PROC", orig) } func TestCPUparseStatLine_424(t *testing.T) { - orig := os.Getenv("HOST_PROC") - os.Setenv("HOST_PROC", "testdata/linux/424/proc") + t.Setenv("HOST_PROC", "testdata/linux/424/proc") { l, err := Times(true) if err != nil || len(l) == 0 { @@ -40,7 +36,6 @@ func TestCPUparseStatLine_424(t *testing.T) { } t.Logf("Times(false): %#v", l) } - os.Setenv("HOST_PROC", orig) } func TestCPUCountsAgainstLscpu(t *testing.T) { @@ -53,7 +48,9 @@ func TestCPUCountsAgainstLscpu(t *testing.T) { } t.Errorf("error executing lscpu: %v", err) } - var threadsPerCore, coresPerSocket, sockets int + var threadsPerCore, coresPerSocket, sockets, books, drawers int + books = 1 + drawers = 1 lines := strings.Split(string(out), "\n") for _, line := range lines { fields := strings.Split(line, ":") @@ -65,14 +62,18 @@ func TestCPUCountsAgainstLscpu(t *testing.T) { threadsPerCore, _ = strconv.Atoi(strings.TrimSpace(fields[1])) case "Core(s) per socket": coresPerSocket, _ = strconv.Atoi(strings.TrimSpace(fields[1])) - case "Socket(s)": + case "Socket(s)", "Socket(s) per book": sockets, _ = strconv.Atoi(strings.TrimSpace(fields[1])) + case "Book(s) per drawer": + books, _ = strconv.Atoi(strings.TrimSpace(fields[1])) + case "Drawer(s)": + drawers, _ = strconv.Atoi(strings.TrimSpace(fields[1])) } } if threadsPerCore == 0 || coresPerSocket == 0 || sockets == 0 { t.Errorf("missing info from lscpu: threadsPerCore=%d coresPerSocket=%d sockets=%d", threadsPerCore, coresPerSocket, sockets) } - expectedPhysical := coresPerSocket * sockets + expectedPhysical := coresPerSocket * sockets * books * drawers expectedLogical := expectedPhysical * threadsPerCore physical, err := Counts(false) skipIfNotImplementedErr(t, err) @@ -93,9 +94,7 @@ func TestCPUCountsAgainstLscpu(t *testing.T) { } func TestCPUCountsLogicalAndroid_1037(t *testing.T) { // https://github.com/shirou/gopsutil/issues/1037 - orig := os.Getenv("HOST_PROC") - os.Setenv("HOST_PROC", "testdata/linux/1037/proc") - defer os.Setenv("HOST_PROC", orig) + t.Setenv("HOST_PROC", "testdata/linux/1037/proc") count, err := Counts(true) if err != nil { diff --git a/cpu/cpu_plan9_test.go b/cpu/cpu_plan9_test.go index 9acf4bf..2820a3f 100644 --- a/cpu/cpu_plan9_test.go +++ b/cpu/cpu_plan9_test.go @@ -4,7 +4,6 @@ package cpu import ( - "os" "path/filepath" "testing" @@ -30,13 +29,9 @@ var timesTests = []struct { } func TestTimesPlan9(t *testing.T) { - origRoot := os.Getenv("HOST_ROOT") - t.Cleanup(func() { - os.Setenv("HOST_ROOT", origRoot) - }) for _, tt := range timesTests { t.Run(tt.mockedRootFS, func(t *testing.T) { - os.Setenv("HOST_ROOT", filepath.Join("testdata/plan9", tt.mockedRootFS)) + t.Setenv("HOST_ROOT", filepath.Join("testdata/plan9", tt.mockedRootFS)) stats, err := Times(false) skipIfNotImplementedErr(t, err) if err != nil { diff --git a/cpu/cpu_test.go b/cpu/cpu_test.go index 91b8e8a..688660a 100644 --- a/cpu/cpu_test.go +++ b/cpu/cpu_test.go @@ -8,8 +8,9 @@ import ( "testing" "time" - "github.com/shirou/gopsutil/v3/internal/common" "github.com/stretchr/testify/assert" + + "github.com/shirou/gopsutil/v3/internal/common" ) func skipIfNotImplementedErr(t *testing.T, err error) { @@ -137,8 +138,8 @@ func testCPUPercent(t *testing.T, percpu bool) { if err != nil { t.Errorf("error %v", err) } - // Skip CircleCI which CPU num is different - if os.Getenv("CIRCLECI") != "true" { + // Skip CI which CPU num is different + if os.Getenv("CI") != "true" { if (percpu && len(v) != numcpu) || (!percpu && len(v) != 1) { t.Fatalf("wrong number of entries from CPUPercent: %v", v) } @@ -171,8 +172,8 @@ func testCPUPercentLastUsed(t *testing.T, percpu bool) { if err != nil { t.Errorf("error %v", err) } - // Skip CircleCI which CPU num is different - if os.Getenv("CIRCLECI") != "true" { + // Skip CI which CPU num is different + if os.Getenv("CI") != "true" { if (percpu && len(v) != numcpu) || (!percpu && len(v) != 1) { t.Fatalf("wrong number of entries from CPUPercent: %v", v) } diff --git a/cpu/cpu_windows.go b/cpu/cpu_windows.go index d1a0e4c..e10612f 100644 --- a/cpu/cpu_windows.go +++ b/cpu/cpu_windows.go @@ -14,8 +14,7 @@ import ( ) var ( - procGetActiveProcessorCount = common.Modkernel32.NewProc("GetActiveProcessorCount") - procGetNativeSystemInfo = common.Modkernel32.NewProc("GetNativeSystemInfo") + procGetNativeSystemInfo = common.Modkernel32.NewProc("GetNativeSystemInfo") ) type win32_Processor struct { @@ -204,15 +203,12 @@ type systemInfo struct { func CountsWithContext(ctx context.Context, logical bool) (int, error) { if logical { // https://github.com/giampaolo/psutil/blob/d01a9eaa35a8aadf6c519839e987a49d8be2d891/psutil/_psutil_windows.c#L97 - err := procGetActiveProcessorCount.Find() - if err == nil { // Win7+ - ret, _, _ := procGetActiveProcessorCount.Call(uintptr(0xffff)) // ALL_PROCESSOR_GROUPS is 0xffff according to Rust's winapi lib https://docs.rs/winapi/*/x86_64-pc-windows-msvc/src/winapi/shared/ntdef.rs.html#120 - if ret != 0 { - return int(ret), nil - } + ret := windows.GetActiveProcessorCount(windows.ALL_PROCESSOR_GROUPS) + if ret != 0 { + return int(ret), nil } var systemInfo systemInfo - _, _, err = procGetNativeSystemInfo.Call(uintptr(unsafe.Pointer(&systemInfo))) + _, _, err := procGetNativeSystemInfo.Call(uintptr(unsafe.Pointer(&systemInfo))) if systemInfo.dwNumberOfProcessors == 0 { return 0, err } diff --git a/disk/disk.go b/disk/disk.go index dd4cc1d..0d4b253 100644 --- a/disk/disk.go +++ b/disk/disk.go @@ -9,6 +9,8 @@ import ( var invoke common.Invoker = common.Invoke{} +type Warnings = common.Warnings + type UsageStat struct { Path string `json:"path"` Fstype string `json:"fstype"` diff --git a/disk/disk_aix_nocgo.go b/disk/disk_aix_nocgo.go index eb25cbd..4f93c75 100644 --- a/disk/disk_aix_nocgo.go +++ b/disk/disk_aix_nocgo.go @@ -5,14 +5,78 @@ package disk import ( "context" + "regexp" + "strings" "github.com/shirou/gopsutil/v3/internal/common" + "golang.org/x/sys/unix" ) +var whiteSpaces = regexp.MustCompile(`\s+`) +var startBlank = regexp.MustCompile(`^\s+`) + +var ignoreFSType = map[string]bool{"procfs": true} +var FSType = map[int]string{ + 0: "jfs2", 1: "namefs", 2: "nfs", 3: "jfs", 5: "cdrom", 6: "proc", + 16: "special-fs", 17: "cache-fs", 18: "nfs3", 19: "automount-fs", 20: "pool-fs", 32: "vxfs", + 33: "veritas-fs", 34: "udfs", 35: "nfs4", 36: "nfs4-pseudo", 37: "smbfs", 38: "mcr-pseudofs", + 39: "ahafs", 40: "sterm-nfs", 41: "asmfs", +} + func PartitionsWithContext(ctx context.Context, all bool) ([]PartitionStat, error) { - return []PartitionStat{}, common.ErrNotImplementedError + var ret []PartitionStat + + out, err := invoke.CommandWithContext(ctx, "mount") + if err != nil { + return nil, err + } + + // parse head lines for column names + colidx := make(map[string]int) + lines := strings.Split(string(out), "\n") + if len(lines) < 3 { + return nil, common.ErrNotImplementedError + } + + idx := 0 + start := 0 + finished := false + for pos, ch := range lines[1] { + if ch == ' ' && !finished { + name := strings.TrimSpace(lines[0][start:pos]) + colidx[name] = idx + finished = true + } else if ch == '-' && finished { + idx++ + start = pos + finished = false + } + } + name := strings.TrimSpace(lines[0][start:len(lines[1])]) + colidx[name] = idx + + for idx := 2; idx < len(lines); idx++ { + line := lines[idx] + if startBlank.MatchString(line) { + line = "localhost" + line + } + p := whiteSpaces.Split(lines[idx], 6) + if len(p) < 5 || ignoreFSType[p[colidx["vfs"]]] { + continue + } + d := PartitionStat{ + Device: p[colidx["mounted"]], + Mountpoint: p[colidx["mounted over"]], + Fstype: p[colidx["vfs"]], + Opts: strings.Split(p[colidx["options"]], ","), + } + + ret = append(ret, d) + } + + return ret, nil } -func UsageWithContext(ctx context.Context, path string) (*UsageStat, error) { - return nil, common.ErrNotImplementedError +func getFsType(stat unix.Statfs_t) string { + return FSType[int(stat.Vfstype)] } diff --git a/disk/disk_darwin.go b/disk/disk_darwin.go index 0877b76..9362d9e 100644 --- a/disk/disk_darwin.go +++ b/disk/disk_darwin.go @@ -6,8 +6,9 @@ package disk import ( "context" - "github.com/shirou/gopsutil/v3/internal/common" "golang.org/x/sys/unix" + + "github.com/shirou/gopsutil/v3/internal/common" ) // PartitionsWithContext returns disk partition. @@ -20,9 +21,15 @@ func PartitionsWithContext(ctx context.Context, all bool) ([]PartitionStat, erro return ret, err } fs := make([]unix.Statfs_t, count) - if _, err = unix.Getfsstat(fs, unix.MNT_WAIT); err != nil { + count, err = unix.Getfsstat(fs, unix.MNT_WAIT) + if err != nil { return ret, err } + // On 10.14, and possibly other OS versions, the actual count may + // be less than from the first call. Truncate to the returned count + // to prevent accessing uninitialized entries. + // https://github.com/shirou/gopsutil/issues/1390 + fs = fs[:count] for _, stat := range fs { opts := []string{"rw"} if stat.Flags&unix.MNT_RDONLY != 0 { diff --git a/disk/disk_darwin_cgo.go b/disk/disk_darwin_cgo.go index b041c8d..27c24c9 100644 --- a/disk/disk_darwin_cgo.go +++ b/disk/disk_darwin_cgo.go @@ -1,5 +1,5 @@ -//go:build darwin && cgo -// +build darwin,cgo +//go:build darwin && cgo && !ios +// +build darwin,cgo,!ios package disk diff --git a/disk/disk_darwin_nocgo.go b/disk/disk_darwin_nocgo.go index 99bb8ba..1f099b7 100644 --- a/disk/disk_darwin_nocgo.go +++ b/disk/disk_darwin_nocgo.go @@ -1,5 +1,5 @@ -//go:build darwin && !cgo -// +build darwin,!cgo +//go:build (darwin && !cgo) || ios +// +build darwin,!cgo ios package disk diff --git a/disk/disk_freebsd.go b/disk/disk_freebsd.go index 753ce9a..9b53106 100644 --- a/disk/disk_freebsd.go +++ b/disk/disk_freebsd.go @@ -12,9 +12,8 @@ import ( "strconv" "strings" - "golang.org/x/sys/unix" - "github.com/shirou/gopsutil/v3/internal/common" + "golang.org/x/sys/unix" ) // PartitionsWithContext returns disk partition. diff --git a/disk/disk_linux.go b/disk/disk_linux.go index 3911af9..5015c34 100644 --- a/disk/disk_linux.go +++ b/disk/disk_linux.go @@ -16,8 +16,9 @@ import ( "strconv" "strings" - "github.com/shirou/gopsutil/v3/internal/common" "golang.org/x/sys/unix" + + "github.com/shirou/gopsutil/v3/internal/common" ) const ( @@ -259,10 +260,10 @@ func readMountFile(root string) (lines []string, useMounts bool, filename string func PartitionsWithContext(ctx context.Context, all bool) ([]PartitionStat, error) { // by default, try "/proc/1/..." first - root := common.HostProc(path.Join("1")) + root := common.HostProcWithContext(ctx, path.Join("1")) // force preference for dirname of HOST_PROC_MOUNTINFO, if set #1271 - hpmPath := os.Getenv("HOST_PROC_MOUNTINFO") + hpmPath := common.HostProcMountInfoWithContext(ctx) if hpmPath != "" { root = filepath.Dir(hpmPath) } @@ -273,13 +274,13 @@ func PartitionsWithContext(ctx context.Context, all bool) ([]PartitionStat, erro return nil, err } // fallback to "/proc/self/..." #1159 - lines, useMounts, filename, err = readMountFile(common.HostProc(path.Join("self"))) + lines, useMounts, filename, err = readMountFile(common.HostProcWithContext(ctx, path.Join("self"))) if err != nil { return nil, err } } - fs, err := getFileSystems() + fs, err := getFileSystems(ctx) if err != nil && !all { return nil, err } @@ -341,7 +342,7 @@ func PartitionsWithContext(ctx context.Context, all bool) ([]PartitionStat, erro } if strings.HasPrefix(d.Device, "/dev/mapper/") { - devpath, err := filepath.EvalSymlinks(common.HostDev(strings.Replace(d.Device, "/dev", "", -1))) + devpath, err := filepath.EvalSymlinks(common.HostDevWithContext(ctx, strings.Replace(d.Device, "/dev", "", 1))) if err == nil { d.Device = devpath } @@ -350,7 +351,7 @@ func PartitionsWithContext(ctx context.Context, all bool) ([]PartitionStat, erro // /dev/root is not the real device name // so we get the real device name from its major/minor number if d.Device == "/dev/root" { - devpath, err := os.Readlink(common.HostSys("/dev/block/" + blockDeviceID)) + devpath, err := os.Readlink(common.HostSysWithContext(ctx, "/dev/block/"+blockDeviceID)) if err == nil { d.Device = strings.Replace(d.Device, "root", filepath.Base(devpath), 1) } @@ -363,8 +364,8 @@ func PartitionsWithContext(ctx context.Context, all bool) ([]PartitionStat, erro } // getFileSystems returns supported filesystems from /proc/filesystems -func getFileSystems() ([]string, error) { - filename := common.HostProc("filesystems") +func getFileSystems(ctx context.Context) ([]string, error) { + filename := common.HostProcWithContext(ctx, "filesystems") lines, err := common.ReadLines(filename) if err != nil { return nil, err @@ -386,7 +387,7 @@ func getFileSystems() ([]string, error) { } func IOCountersWithContext(ctx context.Context, names ...string) (map[string]IOCountersStat, error) { - filename := common.HostProc("diskstats") + filename := common.HostProcWithContext(ctx, "diskstats") lines, err := common.ReadLines(filename) if err != nil { return nil, err @@ -473,7 +474,11 @@ func IOCountersWithContext(ctx context.Context, names ...string) (map[string]IOC } d.Name = name - d.SerialNumber, _ = SerialNumberWithContext(ctx, name) + // Names passed in can be full paths (/dev/sda) or just device names (sda). + // Since `name` here is already a basename, re-add the /dev path. + // This is not ideal, but we may break the API by changing how SerialNumberWithContext + // works. + d.SerialNumber, _ = SerialNumberWithContext(ctx, common.HostDevWithContext(ctx, name)) d.Label, _ = LabelWithContext(ctx, name) ret[name] = d @@ -491,7 +496,7 @@ func SerialNumberWithContext(ctx context.Context, name string) (string, error) { minor := unix.Minor(uint64(stat.Rdev)) // Try to get the serial from udev data - udevDataPath := common.HostRun(fmt.Sprintf("udev/data/b%d:%d", major, minor)) + udevDataPath := common.HostRunWithContext(ctx, fmt.Sprintf("udev/data/b%d:%d", major, minor)) if udevdata, err := ioutil.ReadFile(udevDataPath); err == nil { scanner := bufio.NewScanner(bytes.NewReader(udevdata)) for scanner.Scan() { @@ -504,7 +509,7 @@ func SerialNumberWithContext(ctx context.Context, name string) (string, error) { // Try to get the serial from sysfs, look at the disk device (minor 0) directly // because if it is a partition it is not going to contain any device information - devicePath := common.HostSys(fmt.Sprintf("dev/block/%d:0/device", major)) + devicePath := common.HostSysWithContext(ctx, fmt.Sprintf("dev/block/%d:0/device", major)) model, _ := ioutil.ReadFile(filepath.Join(devicePath, "model")) serial, _ := ioutil.ReadFile(filepath.Join(devicePath, "serial")) if len(model) > 0 && len(serial) > 0 { @@ -515,7 +520,7 @@ func SerialNumberWithContext(ctx context.Context, name string) (string, error) { func LabelWithContext(ctx context.Context, name string) (string, error) { // Try label based on devicemapper name - dmname_filename := common.HostSys(fmt.Sprintf("block/%s/dm/name", name)) + dmname_filename := common.HostSysWithContext(ctx, fmt.Sprintf("block/%s/dm/name", name)) if !common.PathExists(dmname_filename) { return "", nil diff --git a/disk/disk_solaris.go b/disk/disk_solaris.go index 9c4a798..934d651 100644 --- a/disk/disk_solaris.go +++ b/disk/disk_solaris.go @@ -10,6 +10,10 @@ import ( "fmt" "math" "os" + "path/filepath" + "regexp" + "runtime" + "strconv" "strings" "github.com/shirou/gopsutil/v3/internal/common" @@ -73,20 +77,129 @@ func PartitionsWithContext(ctx context.Context, all bool) ([]PartitionStat, erro }) } if err := scanner.Err(); err != nil { - return nil, fmt.Errorf("unable to scan %q: %v", _MNTTAB, err) + return nil, fmt.Errorf("unable to scan %q: %w", _MNTTAB, err) } return ret, err } func IOCountersWithContext(ctx context.Context, names ...string) (map[string]IOCountersStat, error) { - return nil, common.ErrNotImplementedError + var issolaris bool + if runtime.GOOS == "illumos" { + issolaris = false + } else { + issolaris = true + } + // check disks instead of zfs pools + filterstr := "/[^zfs]/:::/^nread$|^nwritten$|^reads$|^writes$|^rtime$|^wtime$/" + kstatSysOut, err := invoke.CommandWithContext(ctx, "kstat", "-c", "disk", "-p", filterstr) + if err != nil { + return nil, fmt.Errorf("cannot execute kstat: %w", err) + } + lines := strings.Split(strings.TrimSpace(string(kstatSysOut)), "\n") + if len(lines) == 0 { + return nil, fmt.Errorf("no disk class found") + } + dnamearr := make(map[string]string) + nreadarr := make(map[string]uint64) + nwrittenarr := make(map[string]uint64) + readsarr := make(map[string]uint64) + writesarr := make(map[string]uint64) + rtimearr := make(map[string]uint64) + wtimearr := make(map[string]uint64) + re := regexp.MustCompile(`[:\s]+`) + + // in case the name is "/dev/sda1", then convert to "sda1" + for i, name := range names { + names[i] = filepath.Base(name) + } + + for _, line := range lines { + fields := re.Split(line, -1) + if len(fields) == 0 { + continue + } + moduleName := fields[0] + instance := fields[1] + dname := fields[2] + + if len(names) > 0 && !common.StringsHas(names, dname) { + continue + } + dnamearr[moduleName+instance] = dname + // fields[3] is the statistic label, fields[4] is the value + switch fields[3] { + case "nread": + nreadarr[moduleName+instance], err = strconv.ParseUint((fields[4]), 10, 64) + if err != nil { + return nil, err + } + case "nwritten": + nwrittenarr[moduleName+instance], err = strconv.ParseUint((fields[4]), 10, 64) + if err != nil { + return nil, err + } + case "reads": + readsarr[moduleName+instance], err = strconv.ParseUint((fields[4]), 10, 64) + if err != nil { + return nil, err + } + case "writes": + writesarr[moduleName+instance], err = strconv.ParseUint((fields[4]), 10, 64) + if err != nil { + return nil, err + } + case "rtime": + if issolaris { + // from sec to milli secs + var frtime float64 + frtime, err = strconv.ParseFloat((fields[4]), 64) + rtimearr[moduleName+instance] = uint64(frtime * 1000) + } else { + // from nano to milli secs + rtimearr[moduleName+instance], err = strconv.ParseUint((fields[4]), 10, 64) + rtimearr[moduleName+instance] = rtimearr[moduleName+instance] / 1000 / 1000 + } + if err != nil { + return nil, err + } + case "wtime": + if issolaris { + // from sec to milli secs + var fwtime float64 + fwtime, err = strconv.ParseFloat((fields[4]), 64) + wtimearr[moduleName+instance] = uint64(fwtime * 1000) + } else { + // from nano to milli secs + wtimearr[moduleName+instance], err = strconv.ParseUint((fields[4]), 10, 64) + wtimearr[moduleName+instance] = wtimearr[moduleName+instance] / 1000 / 1000 + } + if err != nil { + return nil, err + } + } + } + + ret := make(map[string]IOCountersStat, 0) + for k := range dnamearr { + d := IOCountersStat{ + Name: dnamearr[k], + ReadBytes: nreadarr[k], + WriteBytes: nwrittenarr[k], + ReadCount: readsarr[k], + WriteCount: writesarr[k], + ReadTime: rtimearr[k], + WriteTime: wtimearr[k], + } + ret[d.Name] = d + } + return ret, nil } func UsageWithContext(ctx context.Context, path string) (*UsageStat, error) { statvfs := unix.Statvfs_t{} if err := unix.Statvfs(path, &statvfs); err != nil { - return nil, fmt.Errorf("unable to call statvfs(2) on %q: %v", path, err) + return nil, fmt.Errorf("unable to call statvfs(2) on %q: %w", path, err) } usageStat := &UsageStat{ diff --git a/disk/disk_unix.go b/disk/disk_unix.go index 4cef8cb..1e73524 100644 --- a/disk/disk_unix.go +++ b/disk/disk_unix.go @@ -1,5 +1,5 @@ -//go:build freebsd || linux || darwin -// +build freebsd linux darwin +//go:build freebsd || linux || darwin || (aix && !cgo) +// +build freebsd linux darwin aix,!cgo package disk diff --git a/disk/disk_windows.go b/disk/disk_windows.go index 5fb9b5b..e17db3e 100644 --- a/disk/disk_windows.go +++ b/disk/disk_windows.go @@ -50,6 +50,7 @@ func init() { key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Services\PartMgr`, registry.SET_VALUE) if err == nil { key.SetDWordValue("EnableCounterForIoctl", 1) + key.Close() } } @@ -79,66 +80,105 @@ func UsageWithContext(ctx context.Context, path string) (*UsageStat, error) { return ret, nil } +// PartitionsWithContext returns disk partitions. +// Since GetVolumeInformation doesn't have a timeout, this method uses context to set deadline by users. func PartitionsWithContext(ctx context.Context, all bool) ([]PartitionStat, error) { - var ret []PartitionStat - lpBuffer := make([]byte, 254) - diskret, _, err := procGetLogicalDriveStringsW.Call( - uintptr(len(lpBuffer)), - uintptr(unsafe.Pointer(&lpBuffer[0]))) - if diskret == 0 { - return ret, err + warnings := Warnings{ + Verbose: true, } - for _, v := range lpBuffer { - if v >= 65 && v <= 90 { - path := string(v) + ":" - typepath, _ := windows.UTF16PtrFromString(path) - typeret, _, _ := procGetDriveType.Call(uintptr(unsafe.Pointer(typepath))) - if typeret == 0 { - return ret, windows.GetLastError() - } - // 2: DRIVE_REMOVABLE 3: DRIVE_FIXED 4: DRIVE_REMOTE 5: DRIVE_CDROM - - if typeret == 2 || typeret == 3 || typeret == 4 || typeret == 5 { - lpVolumeNameBuffer := make([]byte, 256) - lpVolumeSerialNumber := int64(0) - lpMaximumComponentLength := int64(0) - lpFileSystemFlags := int64(0) - lpFileSystemNameBuffer := make([]byte, 256) - volpath, _ := windows.UTF16PtrFromString(string(v) + ":/") - driveret, _, err := procGetVolumeInformation.Call( - uintptr(unsafe.Pointer(volpath)), - uintptr(unsafe.Pointer(&lpVolumeNameBuffer[0])), - uintptr(len(lpVolumeNameBuffer)), - uintptr(unsafe.Pointer(&lpVolumeSerialNumber)), - uintptr(unsafe.Pointer(&lpMaximumComponentLength)), - uintptr(unsafe.Pointer(&lpFileSystemFlags)), - uintptr(unsafe.Pointer(&lpFileSystemNameBuffer[0])), - uintptr(len(lpFileSystemNameBuffer))) - if driveret == 0 { - if typeret == 5 || typeret == 2 { - continue // device is not ready will happen if there is no disk in the drive - } - return ret, err - } - opts := []string{"rw"} - if lpFileSystemFlags&fileReadOnlyVolume != 0 { - opts = []string{"ro"} + + var errLogicalDrives error + retChan := make(chan PartitionStat) + quitChan := make(chan struct{}) + defer close(quitChan) + + getPartitions := func() { + defer close(retChan) + + lpBuffer := make([]byte, 254) + + diskret, _, err := procGetLogicalDriveStringsW.Call( + uintptr(len(lpBuffer)), + uintptr(unsafe.Pointer(&lpBuffer[0]))) + if diskret == 0 { + errLogicalDrives = err + return + } + for _, v := range lpBuffer { + if v >= 65 && v <= 90 { + path := string(v) + ":" + typepath, _ := windows.UTF16PtrFromString(path) + typeret, _, _ := procGetDriveType.Call(uintptr(unsafe.Pointer(typepath))) + if typeret == 0 { + err := windows.GetLastError() + warnings.Add(err) + continue } - if lpFileSystemFlags&fileFileCompression != 0 { - opts = append(opts, "compress") + // 2: DRIVE_REMOVABLE 3: DRIVE_FIXED 4: DRIVE_REMOTE 5: DRIVE_CDROM + + if typeret == 2 || typeret == 3 || typeret == 4 || typeret == 5 { + lpVolumeNameBuffer := make([]byte, 256) + lpVolumeSerialNumber := int64(0) + lpMaximumComponentLength := int64(0) + lpFileSystemFlags := int64(0) + lpFileSystemNameBuffer := make([]byte, 256) + volpath, _ := windows.UTF16PtrFromString(string(v) + ":/") + driveret, _, err := procGetVolumeInformation.Call( + uintptr(unsafe.Pointer(volpath)), + uintptr(unsafe.Pointer(&lpVolumeNameBuffer[0])), + uintptr(len(lpVolumeNameBuffer)), + uintptr(unsafe.Pointer(&lpVolumeSerialNumber)), + uintptr(unsafe.Pointer(&lpMaximumComponentLength)), + uintptr(unsafe.Pointer(&lpFileSystemFlags)), + uintptr(unsafe.Pointer(&lpFileSystemNameBuffer[0])), + uintptr(len(lpFileSystemNameBuffer))) + if driveret == 0 { + if typeret == 5 || typeret == 2 { + continue // device is not ready will happen if there is no disk in the drive + } + warnings.Add(err) + continue + } + opts := []string{"rw"} + if lpFileSystemFlags&fileReadOnlyVolume != 0 { + opts = []string{"ro"} + } + if lpFileSystemFlags&fileFileCompression != 0 { + opts = append(opts, "compress") + } + + select { + case retChan <- PartitionStat{ + Mountpoint: path, + Device: path, + Fstype: string(bytes.ReplaceAll(lpFileSystemNameBuffer, []byte("\x00"), []byte(""))), + Opts: opts, + }: + case <-quitChan: + return + } } + } + } + } - d := PartitionStat{ - Mountpoint: path, - Device: path, - Fstype: string(bytes.Replace(lpFileSystemNameBuffer, []byte("\x00"), []byte(""), -1)), - Opts: opts, + go getPartitions() + + var ret []PartitionStat + for { + select { + case p, ok := <-retChan: + if !ok { + if errLogicalDrives != nil { + return ret, errLogicalDrives } - ret = append(ret, d) + return ret, warnings.Reference() } + ret = append(ret, p) + case <-ctx.Done(): + return ret, ctx.Err() } } - return ret, nil } func IOCountersWithContext(ctx context.Context, names ...string) (map[string]IOCountersStat, error) { diff --git a/docker/docker_linux.go b/docker/docker_linux.go index ac7cc98..4904874 100644 --- a/docker/docker_linux.go +++ b/docker/docker_linux.go @@ -100,7 +100,7 @@ func CgroupCPUUsage(containerID string, base string) (float64, error) { } func CgroupCPUWithContext(ctx context.Context, containerID string, base string) (*CgroupCPUStat, error) { - statfile := getCgroupFilePath(containerID, base, "cpuacct", "cpuacct.stat") + statfile := getCgroupFilePath(ctx, containerID, base, "cpuacct", "cpuacct.stat") lines, err := common.ReadLines(statfile) if err != nil { return nil, err @@ -136,7 +136,7 @@ func CgroupCPUWithContext(ctx context.Context, containerID string, base string) } func CgroupCPUUsageWithContext(ctx context.Context, containerID, base string) (float64, error) { - usagefile := getCgroupFilePath(containerID, base, "cpuacct", "cpuacct.usage") + usagefile := getCgroupFilePath(ctx, containerID, base, "cpuacct", "cpuacct.usage") lines, err := common.ReadLinesOffsetN(usagefile, 0, 1) if err != nil { return 0.0, err @@ -159,11 +159,11 @@ func CgroupCPUUsageDocker(containerid string) (float64, error) { } func CgroupCPUDockerWithContext(ctx context.Context, containerid string) (*CgroupCPUStat, error) { - return CgroupCPUWithContext(ctx, containerid, common.HostSys("fs/cgroup/cpuacct/docker")) + return CgroupCPUWithContext(ctx, containerid, common.HostSysWithContext(ctx, "fs/cgroup/cpuacct/docker")) } func CgroupCPUDockerUsageWithContext(ctx context.Context, containerid string) (float64, error) { - return CgroupCPUUsageWithContext(ctx, containerid, common.HostSys("fs/cgroup/cpuacct/docker")) + return CgroupCPUUsageWithContext(ctx, containerid, common.HostSysWithContext(ctx, "fs/cgroup/cpuacct/docker")) } func CgroupMem(containerID string, base string) (*CgroupMemStat, error) { @@ -171,7 +171,7 @@ func CgroupMem(containerID string, base string) (*CgroupMemStat, error) { } func CgroupMemWithContext(ctx context.Context, containerID string, base string) (*CgroupMemStat, error) { - statfile := getCgroupFilePath(containerID, base, "memory", "memory.stat") + statfile := getCgroupFilePath(ctx, containerID, base, "memory", "memory.stat") // empty containerID means all cgroup if len(containerID) == 0 { @@ -246,19 +246,19 @@ func CgroupMemWithContext(ctx context.Context, containerID string, base string) } } - r, err := getCgroupMemFile(containerID, base, "memory.usage_in_bytes") + r, err := getCgroupMemFile(ctx, containerID, base, "memory.usage_in_bytes") if err == nil { ret.MemUsageInBytes = r } - r, err = getCgroupMemFile(containerID, base, "memory.max_usage_in_bytes") + r, err = getCgroupMemFile(ctx, containerID, base, "memory.max_usage_in_bytes") if err == nil { ret.MemMaxUsageInBytes = r } - r, err = getCgroupMemFile(containerID, base, "memory.limit_in_bytes") + r, err = getCgroupMemFile(ctx, containerID, base, "memory.limit_in_bytes") if err == nil { ret.MemLimitInBytes = r } - r, err = getCgroupMemFile(containerID, base, "memory.failcnt") + r, err = getCgroupMemFile(ctx, containerID, base, "memory.failcnt") if err == nil { ret.MemFailCnt = r } @@ -271,27 +271,27 @@ func CgroupMemDocker(containerID string) (*CgroupMemStat, error) { } func CgroupMemDockerWithContext(ctx context.Context, containerID string) (*CgroupMemStat, error) { - return CgroupMemWithContext(ctx, containerID, common.HostSys("fs/cgroup/memory/docker")) + return CgroupMemWithContext(ctx, containerID, common.HostSysWithContext(ctx, "fs/cgroup/memory/docker")) } // getCgroupFilePath constructs file path to get targeted stats file. -func getCgroupFilePath(containerID, base, target, file string) string { +func getCgroupFilePath(ctx context.Context, containerID, base, target, file string) string { if len(base) == 0 { - base = common.HostSys(fmt.Sprintf("fs/cgroup/%s/docker", target)) + base = common.HostSysWithContext(ctx, fmt.Sprintf("fs/cgroup/%s/docker", target)) } statfile := path.Join(base, containerID, file) if _, err := os.Stat(statfile); os.IsNotExist(err) { statfile = path.Join( - common.HostSys(fmt.Sprintf("fs/cgroup/%s/system.slice", target)), "docker-"+containerID+".scope", file) + common.HostSysWithContext(ctx, fmt.Sprintf("fs/cgroup/%s/system.slice", target)), "docker-"+containerID+".scope", file) } return statfile } // getCgroupMemFile reads a cgroup file and return the contents as uint64. -func getCgroupMemFile(containerID, base, file string) (uint64, error) { - statfile := getCgroupFilePath(containerID, base, "memory", file) +func getCgroupMemFile(ctx context.Context, containerID, base, file string) (uint64, error) { + statfile := getCgroupFilePath(ctx, containerID, base, "memory", file) lines, err := common.ReadLines(statfile) if err != nil { return 0, err diff --git a/docker/docker_linux_test.go b/docker/docker_linux_test.go index c6afb44..5ef80f9 100644 --- a/docker/docker_linux_test.go +++ b/docker/docker_linux_test.go @@ -3,7 +3,10 @@ package docker -import "testing" +import ( + "context" + "testing" +) func TestGetDockerIDList(t *testing.T) { // If there is not docker environment, this test always fail. @@ -43,7 +46,7 @@ func TestGetDockerStat(t *testing.T) { func TestCgroupCPU(t *testing.T) { v, _ := GetDockerIDList() for _, id := range v { - v, err := CgroupCPUDocker(id) + v, err := CgroupCPUDockerWithContext(context.Background(), id) if err != nil { t.Errorf("error %v", err) } @@ -55,7 +58,7 @@ func TestCgroupCPU(t *testing.T) { } func TestCgroupCPUInvalidId(t *testing.T) { - _, err := CgroupCPUDocker("bad id") + _, err := CgroupCPUDockerWithContext(context.Background(), "bad id") if err == nil { t.Error("Expected path does not exist error") } diff --git a/docker/docker_notlinux.go b/docker/docker_notlinux.go index 2bd9111..434ca12 100644 --- a/docker/docker_notlinux.go +++ b/docker/docker_notlinux.go @@ -46,7 +46,7 @@ func CgroupCPUDocker(containerid string) (*CgroupCPUStat, error) { } func CgroupCPUDockerWithContext(ctx context.Context, containerid string) (*CgroupCPUStat, error) { - return CgroupCPU(containerid, common.HostSys("fs/cgroup/cpuacct/docker")) + return CgroupCPUWithContext(ctx, containerid, common.HostSysWithContext(ctx, "fs/cgroup/cpuacct/docker")) } func CgroupMem(containerid string, base string) (*CgroupMemStat, error) { @@ -62,5 +62,5 @@ func CgroupMemDocker(containerid string) (*CgroupMemStat, error) { } func CgroupMemDockerWithContext(ctx context.Context, containerid string) (*CgroupMemStat, error) { - return CgroupMem(containerid, common.HostSys("fs/cgroup/memory/docker")) + return CgroupMemWithContext(ctx, containerid, common.HostSysWithContext(ctx, "fs/cgroup/memory/docker")) } diff --git a/go.mod b/go.mod index 81e0f03..8fc781e 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,14 @@ module github.com/shirou/gopsutil/v3 go 1.15 require ( - github.com/google/go-cmp v0.5.8 + github.com/google/go-cmp v0.5.9 github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c - github.com/stretchr/testify v1.8.0 - github.com/tklauser/go-sysconf v0.3.10 - github.com/yusufpapurcu/wmi v1.2.2 - golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a + github.com/shoenig/go-m1cpu v0.1.6 + github.com/stretchr/testify v1.8.4 + github.com/tklauser/go-sysconf v0.3.12 + github.com/yusufpapurcu/wmi v1.2.3 + golang.org/x/sys v0.11.0 ) + +retract v3.22.11 diff --git a/go.sum b/go.sum index c8f6a13..914427d 100644 --- a/go.sum +++ b/go.sum @@ -4,32 +4,36 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs 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/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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/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= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= 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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw= -github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= -github.com/tklauser/numcpus v0.4.0 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq//o= -github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= -github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= -github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/host/host.go b/host/host.go index 7c53e20..c7e84e3 100644 --- a/host/host.go +++ b/host/host.go @@ -11,6 +11,8 @@ import ( "github.com/shirou/gopsutil/v3/internal/common" ) +type Warnings = common.Warnings + var invoke common.Invoker = common.Invoke{} // A HostInfoStat describes the host status. @@ -155,3 +157,7 @@ func SensorsTemperatures() ([]TemperatureStat, error) { func timeSince(ts uint64) uint64 { return uint64(time.Now().Unix()) - ts } + +func timeSinceMillis(ts uint64) uint64 { + return uint64(time.Now().UnixMilli()) - ts +} diff --git a/host/host_darwin.go b/host/host_darwin.go index 2f20fc6..1be2e85 100644 --- a/host/host_darwin.go +++ b/host/host_darwin.go @@ -13,9 +13,10 @@ import ( "strings" "unsafe" + "golang.org/x/sys/unix" + "github.com/shirou/gopsutil/v3/internal/common" "github.com/shirou/gopsutil/v3/process" - "golang.org/x/sys/unix" ) // from utmpx.h diff --git a/host/host_linux.go b/host/host_linux.go index 940415c..e6ac63a 100644 --- a/host/host_linux.go +++ b/host/host_linux.go @@ -15,8 +15,9 @@ import ( "strconv" "strings" - "github.com/shirou/gopsutil/v3/internal/common" "golang.org/x/sys/unix" + + "github.com/shirou/gopsutil/v3/internal/common" ) type lsbStruct struct { @@ -34,9 +35,9 @@ const ( ) func HostIDWithContext(ctx context.Context) (string, error) { - sysProductUUID := common.HostSys("class/dmi/id/product_uuid") - machineID := common.HostEtc("machine-id") - procSysKernelRandomBootID := common.HostProc("sys/kernel/random/boot_id") + sysProductUUID := common.HostSysWithContext(ctx, "class/dmi/id/product_uuid") + machineID := common.HostEtcWithContext(ctx, "machine-id") + procSysKernelRandomBootID := common.HostProcWithContext(ctx, "sys/kernel/random/boot_id") switch { // In order to read this file, needs to be supported by kernel/arch and run as root // so having fallback is important @@ -66,7 +67,7 @@ func HostIDWithContext(ctx context.Context) (string, error) { } func numProcs(ctx context.Context) (uint64, error) { - return common.NumProcs() + return common.NumProcsWithContext(ctx) } func BootTimeWithContext(ctx context.Context) (uint64, error) { @@ -82,7 +83,7 @@ func UptimeWithContext(ctx context.Context) (uint64, error) { } func UsersWithContext(ctx context.Context) ([]UserStat, error) { - utmpfile := common.HostVar("run/utmp") + utmpfile := common.HostVarWithContext(ctx, "run/utmp") file, err := os.Open(utmpfile) if err != nil { @@ -123,10 +124,10 @@ func UsersWithContext(ctx context.Context) ([]UserStat, error) { return ret, nil } -func getlsbStruct() (*lsbStruct, error) { +func getlsbStruct(ctx context.Context) (*lsbStruct, error) { ret := &lsbStruct{} - if common.PathExists(common.HostEtc("lsb-release")) { - contents, err := common.ReadLines(common.HostEtc("lsb-release")) + if common.PathExists(common.HostEtcWithContext(ctx, "lsb-release")) { + contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "lsb-release")) if err != nil { return ret, err // return empty } @@ -137,13 +138,13 @@ func getlsbStruct() (*lsbStruct, error) { } switch field[0] { case "DISTRIB_ID": - ret.ID = field[1] + ret.ID = strings.ReplaceAll(field[1], `"`, ``) case "DISTRIB_RELEASE": - ret.Release = field[1] + ret.Release = strings.ReplaceAll(field[1], `"`, ``) case "DISTRIB_CODENAME": - ret.Codename = field[1] + ret.Codename = strings.ReplaceAll(field[1], `"`, ``) case "DISTRIB_DESCRIPTION": - ret.Description = field[1] + ret.Description = strings.ReplaceAll(field[1], `"`, ``) } } } else if common.PathExists("/usr/bin/lsb_release") { @@ -158,13 +159,13 @@ func getlsbStruct() (*lsbStruct, error) { } switch field[0] { case "Distributor ID": - ret.ID = field[1] + ret.ID = strings.ReplaceAll(field[1], `"`, ``) case "Release": - ret.Release = field[1] + ret.Release = strings.ReplaceAll(field[1], `"`, ``) case "Codename": - ret.Codename = field[1] + ret.Codename = strings.ReplaceAll(field[1], `"`, ``) case "Description": - ret.Description = field[1] + ret.Description = strings.ReplaceAll(field[1], `"`, ``) } } @@ -174,90 +175,96 @@ func getlsbStruct() (*lsbStruct, error) { } func PlatformInformationWithContext(ctx context.Context) (platform string, family string, version string, err error) { - lsb, err := getlsbStruct() + lsb, err := getlsbStruct(ctx) if err != nil { lsb = &lsbStruct{} } - if common.PathExistsWithContents(common.HostEtc("oracle-release")) { + if common.PathExistsWithContents(common.HostEtcWithContext(ctx, "oracle-release")) { platform = "oracle" - contents, err := common.ReadLines(common.HostEtc("oracle-release")) + contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "oracle-release")) if err == nil { version = getRedhatishVersion(contents) } - } else if common.PathExistsWithContents(common.HostEtc("enterprise-release")) { + } else if common.PathExistsWithContents(common.HostEtcWithContext(ctx, "enterprise-release")) { platform = "oracle" - contents, err := common.ReadLines(common.HostEtc("enterprise-release")) + contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "enterprise-release")) if err == nil { version = getRedhatishVersion(contents) } - } else if common.PathExistsWithContents(common.HostEtc("slackware-version")) { + } else if common.PathExistsWithContents(common.HostEtcWithContext(ctx, "slackware-version")) { platform = "slackware" - contents, err := common.ReadLines(common.HostEtc("slackware-version")) + contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "slackware-version")) if err == nil { version = getSlackwareVersion(contents) } - } else if common.PathExistsWithContents(common.HostEtc("debian_version")) { + } else if common.PathExistsWithContents(common.HostEtcWithContext(ctx, "debian_version")) { if lsb.ID == "Ubuntu" { platform = "ubuntu" version = lsb.Release } else if lsb.ID == "LinuxMint" { platform = "linuxmint" version = lsb.Release + } else if lsb.ID == "Kylin" { + platform = "Kylin" + version = lsb.Release + } else if lsb.ID == `"Cumulus Linux"` { + platform = "cumuluslinux" + version = lsb.Release } else { if common.PathExistsWithContents("/usr/bin/raspi-config") { platform = "raspbian" } else { platform = "debian" } - contents, err := common.ReadLines(common.HostEtc("debian_version")) + contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "debian_version")) if err == nil && len(contents) > 0 && contents[0] != "" { version = contents[0] } } - } else if common.PathExists(common.HostEtc("neokylin-release")) { - contents, err := common.ReadLines(common.HostEtc("neokylin-release")) + } else if common.PathExists(common.HostEtcWithContext(ctx, "neokylin-release")) { + contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "neokylin-release")) if err == nil { version = getRedhatishVersion(contents) platform = getRedhatishPlatform(contents) } - } else if common.PathExists(common.HostEtc("redhat-release")) { - contents, err := common.ReadLines(common.HostEtc("redhat-release")) + } else if common.PathExists(common.HostEtcWithContext(ctx, "redhat-release")) { + contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "redhat-release")) if err == nil { version = getRedhatishVersion(contents) platform = getRedhatishPlatform(contents) } - } else if common.PathExists(common.HostEtc("system-release")) { - contents, err := common.ReadLines(common.HostEtc("system-release")) + } else if common.PathExists(common.HostEtcWithContext(ctx, "system-release")) { + contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "system-release")) if err == nil { version = getRedhatishVersion(contents) platform = getRedhatishPlatform(contents) } - } else if common.PathExists(common.HostEtc("gentoo-release")) { + } else if common.PathExists(common.HostEtcWithContext(ctx, "gentoo-release")) { platform = "gentoo" - contents, err := common.ReadLines(common.HostEtc("gentoo-release")) + contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "gentoo-release")) if err == nil { version = getRedhatishVersion(contents) } - } else if common.PathExists(common.HostEtc("SuSE-release")) { - contents, err := common.ReadLines(common.HostEtc("SuSE-release")) + } else if common.PathExists(common.HostEtcWithContext(ctx, "SuSE-release")) { + contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "SuSE-release")) if err == nil { version = getSuseVersion(contents) platform = getSusePlatform(contents) } // TODO: slackware detecion - } else if common.PathExists(common.HostEtc("arch-release")) { + } else if common.PathExists(common.HostEtcWithContext(ctx, "arch-release")) { platform = "arch" version = lsb.Release - } else if common.PathExists(common.HostEtc("alpine-release")) { + } else if common.PathExists(common.HostEtcWithContext(ctx, "alpine-release")) { platform = "alpine" - contents, err := common.ReadLines(common.HostEtc("alpine-release")) + contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "alpine-release")) if err == nil && len(contents) > 0 && contents[0] != "" { version = contents[0] } - } else if common.PathExists(common.HostEtc("os-release")) { - p, v, err := common.GetOSRelease() + } else if common.PathExists(common.HostEtcWithContext(ctx, "os-release")) { + p, v, err := common.GetOSReleaseWithContext(ctx) if err == nil { platform = p version = v @@ -282,7 +289,7 @@ func PlatformInformationWithContext(ctx context.Context) (platform string, famil platform = strings.Trim(platform, `"`) switch platform { - case "debian", "ubuntu", "linuxmint", "raspbian": + case "debian", "ubuntu", "linuxmint", "raspbian", "Kylin", "cumuluslinux": family = "debian" case "fedora": family = "fedora" @@ -317,7 +324,7 @@ func KernelVersionWithContext(ctx context.Context) (version string, err error) { if err != nil { return "", err } - return string(utsname.Release[:bytes.IndexByte(utsname.Release[:], 0)]), nil + return unix.ByteSliceToString(utsname.Release[:]), nil } func getSlackwareVersion(contents []string) string { @@ -383,14 +390,14 @@ func SensorsTemperaturesWithContext(ctx context.Context) ([]TemperatureStat, err // Only the temp*_input file provides current temperature // value in millidegree Celsius as reported by the temperature to the device: // https://www.kernel.org/doc/Documentation/hwmon/sysfs-interface - if files, err = filepath.Glob(common.HostSys("/class/hwmon/hwmon*/temp*_input")); err != nil { + if files, err = filepath.Glob(common.HostSysWithContext(ctx, "/class/hwmon/hwmon*/temp*_input")); err != nil { return temperatures, err } if len(files) == 0 { // CentOS has an intermediate /device directory: // https://github.com/giampaolo/psutil/issues/971 - if files, err = filepath.Glob(common.HostSys("/class/hwmon/hwmon*/device/temp*_input")); err != nil { + if files, err = filepath.Glob(common.HostSysWithContext(ctx, "/class/hwmon/hwmon*/device/temp*_input")); err != nil { return temperatures, err } } @@ -398,7 +405,7 @@ func SensorsTemperaturesWithContext(ctx context.Context) ([]TemperatureStat, err var warns Warnings if len(files) == 0 { // handle distributions without hwmon, like raspbian #391, parse legacy thermal_zone files - files, err = filepath.Glob(common.HostSys("/class/thermal/thermal_zone*/")) + files, err = filepath.Glob(common.HostSysWithContext(ctx, "/class/thermal/thermal_zone*/")) if err != nil { return temperatures, err } diff --git a/host/host_linux_loong64.go b/host/host_linux_loong64.go new file mode 100644 index 0000000..edf1be5 --- /dev/null +++ b/host/host_linux_loong64.go @@ -0,0 +1,48 @@ +// Code generated by cmd/cgo -godefs; DO NOT EDIT. +// cgo -godefs host/types_linux.go + +//go:build linux && loong64 +// +build linux,loong64 + +package host + +const ( + sizeofPtr = 0x8 + sizeofShort = 0x2 + sizeofInt = 0x4 + sizeofLong = 0x8 + sizeofLongLong = 0x8 + sizeOfUtmp = 0x190 +) + +type ( + _C_short int16 + _C_int int32 + _C_long int64 + _C_long_long int64 +) + +type ( + utmp struct { + Type int16 + Pid int32 + Line [32]int8 + Id [4]int8 + User [32]int8 + Host [256]int8 + Exit exit_status + Session int64 + Tv timeval + Addr_v6 [4]int32 + X__glibc_reserved [20]int8 + Pad_cgo_0 [4]byte + } + exit_status struct { + Termination int16 + Exit int16 + } + timeval struct { + Sec int64 + Usec int64 + } +) diff --git a/host/host_linux_ppc64.go b/host/host_linux_ppc64.go new file mode 100644 index 0000000..5b324ef --- /dev/null +++ b/host/host_linux_ppc64.go @@ -0,0 +1,48 @@ +//go:build linux && ppc64 +// +build linux,ppc64 + +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_linux.go + +package host + +const ( + sizeofPtr = 0x8 + sizeofShort = 0x2 + sizeofInt = 0x4 + sizeofLong = 0x8 + sizeofLongLong = 0x8 + sizeOfUtmp = 0x180 +) + +type ( + _C_short int16 + _C_int int32 + _C_long int64 + _C_long_long int64 +) + +type utmp struct { + Type int16 + Pad_cgo_0 [2]byte + Pid int32 + Line [32]int8 + Id [4]int8 + User [32]int8 + Host [256]int8 + Exit exit_status + Session int32 + Tv timeval + Addr_v6 [4]int32 + X__glibc_reserved [20]int8 +} + +type exit_status struct { + Termination int16 + Exit int16 +} + +type timeval struct { + Sec int64 + Usec int64 +} diff --git a/host/host_linux_test.go b/host/host_linux_test.go index 8c23e56..c114ec7 100644 --- a/host/host_linux_test.go +++ b/host/host_linux_test.go @@ -4,7 +4,10 @@ package host import ( + "context" "testing" + + "github.com/shirou/gopsutil/v3/common" ) func TestGetRedhatishVersion(t *testing.T) { @@ -60,3 +63,45 @@ func TestGetRedhatishPlatform(t *testing.T) { t.Errorf("Could not get platform with no value: %v", ret) } } + +func Test_getlsbStruct(t *testing.T) { + cases := []struct { + root string + id string + release string + codename string + description string + }{ + {"arch", "Arch", "rolling", "", "Arch Linux"}, + {"ubuntu_22_04", "Ubuntu", "22.04", "jammy", "Ubuntu 22.04.2 LTS"}, + } + + for _, tt := range cases { + tt := tt + t.Run(tt.root, func(t *testing.T) { + ctx := context.WithValue(context.Background(), + common.EnvKey, + common.EnvMap{common.HostEtcEnvKey: "./testdata/linux/lsbStruct/" + tt.root}, + ) + + v, err := getlsbStruct(ctx) + if err != nil { + t.Errorf("error %v", err) + } + if v.ID != tt.id { + t.Errorf("ID: want %v, got %v", tt.id, v.ID) + } + if v.Release != tt.release { + t.Errorf("Release: want %v, got %v", tt.release, v.Release) + } + if v.Codename != tt.codename { + t.Errorf("Codename: want %v, got %v", tt.codename, v.Codename) + } + if v.Description != tt.description { + t.Errorf("Description: want %v, got %v", tt.description, v.Description) + } + + t.Log(v) + }) + } +} diff --git a/host/host_posix.go b/host/host_posix.go index 89e6378..24529f1 100644 --- a/host/host_posix.go +++ b/host/host_posix.go @@ -3,14 +3,13 @@ package host -import ( - "bytes" - - "golang.org/x/sys/unix" -) +import "golang.org/x/sys/unix" func KernelArch() (string, error) { var utsname unix.Utsname err := unix.Uname(&utsname) - return string(utsname.Machine[:bytes.IndexByte(utsname.Machine[:], 0)]), err + if err != nil { + return "", err + } + return unix.ByteSliceToString(utsname.Machine[:]), nil } diff --git a/host/host_test.go b/host/host_test.go index 7630789..17ced6d 100644 --- a/host/host_test.go +++ b/host/host_test.go @@ -29,10 +29,11 @@ func TestHostInfo(t *testing.T) { if v.Procs == 0 { t.Errorf("Could not determine the number of host processes") } + t.Log(v) } func TestUptime(t *testing.T) { - if os.Getenv("CIRCLECI") == "true" { + if os.Getenv("CI") == "true" { t.Skip("Skip CI") } @@ -47,7 +48,7 @@ func TestUptime(t *testing.T) { } func TestBoot_time(t *testing.T) { - if os.Getenv("CIRCLECI") == "true" { + if os.Getenv("CI") == "true" { t.Skip("Skip CI") } v, err := BootTime() diff --git a/host/host_windows.go b/host/host_windows.go index fcd1d59..1fe0551 100644 --- a/host/host_windows.go +++ b/host/host_windows.go @@ -103,6 +103,14 @@ func numProcs(ctx context.Context) (uint64, error) { } func UptimeWithContext(ctx context.Context) (uint64, error) { + up, err := uptimeMillis() + if err != nil { + return 0, err + } + return uint64((time.Duration(up) * time.Millisecond).Seconds()), nil +} + +func uptimeMillis() (uint64, error) { procGetTickCount := procGetTickCount64 err := procGetTickCount64.Find() if err != nil { @@ -112,7 +120,7 @@ func UptimeWithContext(ctx context.Context) (uint64, error) { if lastErr != 0 { return 0, lastErr } - return uint64((time.Duration(r1) * time.Millisecond).Seconds()), nil + return uint64(r1), nil } // cachedBootTime must be accessed via atomic.Load/StoreUint64 @@ -123,11 +131,11 @@ func BootTimeWithContext(ctx context.Context) (uint64, error) { if t != 0 { return t, nil } - up, err := Uptime() + up, err := uptimeMillis() if err != nil { return 0, err } - t = timeSince(up) + t = uint64((time.Duration(timeSinceMillis(up)) * time.Millisecond).Seconds()) atomic.StoreUint64(&cachedBootTime, t) return t, nil } @@ -188,6 +196,14 @@ func PlatformInformationWithContext(ctx context.Context) (platform string, famil } } + var UBR uint32 // Update Build Revision + err = windows.RegQueryValueEx(h, windows.StringToUTF16Ptr(`UBR`), nil, &valType, nil, &bufLen) + if err == nil { + regBuf := make([]byte, 4) + err = windows.RegQueryValueEx(h, windows.StringToUTF16Ptr(`UBR`), nil, &valType, (*byte)(unsafe.Pointer(®Buf[0])), &bufLen) + copy((*[4]byte)(unsafe.Pointer(&UBR))[:], regBuf) + } + // PlatformFamily switch osInfo.wProductType { case 1: @@ -199,7 +215,9 @@ func PlatformInformationWithContext(ctx context.Context) (platform string, famil } // Platform Version - version = fmt.Sprintf("%d.%d.%d Build %d", osInfo.dwMajorVersion, osInfo.dwMinorVersion, osInfo.dwBuildNumber, osInfo.dwBuildNumber) + version = fmt.Sprintf("%d.%d.%d.%d Build %d.%d", + osInfo.dwMajorVersion, osInfo.dwMinorVersion, osInfo.dwBuildNumber, UBR, + osInfo.dwBuildNumber, UBR) return platform, family, version, nil } diff --git a/host/testdata/linux/lsbStruct/arch/lsb-release b/host/testdata/linux/lsbStruct/arch/lsb-release new file mode 100644 index 0000000..01356b2 --- /dev/null +++ b/host/testdata/linux/lsbStruct/arch/lsb-release @@ -0,0 +1,3 @@ +DISTRIB_ID="Arch" +DISTRIB_RELEASE="rolling" +DISTRIB_DESCRIPTION="Arch Linux" diff --git a/host/testdata/linux/lsbStruct/ubuntu_22_04/lsb-release b/host/testdata/linux/lsbStruct/ubuntu_22_04/lsb-release new file mode 100644 index 0000000..0575ec5 --- /dev/null +++ b/host/testdata/linux/lsbStruct/ubuntu_22_04/lsb-release @@ -0,0 +1,4 @@ +DISTRIB_ID=Ubuntu +DISTRIB_RELEASE=22.04 +DISTRIB_CODENAME=jammy +DISTRIB_DESCRIPTION="Ubuntu 22.04.2 LTS" \ No newline at end of file diff --git a/internal/common/binary.go b/internal/common/binary.go index 446c359..5e8d43d 100644 --- a/internal/common/binary.go +++ b/internal/common/binary.go @@ -21,6 +21,7 @@ package common // high-performance serialization, especially for large data structures, // should look at more advanced solutions such as the encoding/gob // package or protocol buffers. + import ( "errors" "io" diff --git a/internal/common/common.go b/internal/common/common.go index db93b67..7a31d25 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -6,6 +6,7 @@ package common // - linux (amd64, arm) // - freebsd (amd64) // - windows (amd64) + import ( "bufio" "bytes" @@ -24,6 +25,8 @@ import ( "strconv" "strings" "time" + + "github.com/shirou/gopsutil/v3/common" ) var ( @@ -111,6 +114,30 @@ func ReadLines(filename string) ([]string, error) { return ReadLinesOffsetN(filename, 0, -1) } +// ReadLine reads a file and returns the first occurrence of a line that is prefixed with prefix. +func ReadLine(filename string, prefix string) (string, error) { + f, err := os.Open(filename) + if err != nil { + return "", err + } + defer f.Close() + r := bufio.NewReader(f) + for { + line, err := r.ReadString('\n') + if err != nil { + if err == io.EOF { + break + } + return "", err + } + if strings.HasPrefix(line, prefix) { + return line, nil + } + } + + return "", nil +} + // ReadLinesOffsetN reads contents from file and splits them by new line. // The offset tells at which line number to start. // The count determines the number of lines to read (starting from offset): @@ -320,6 +347,23 @@ func PathExistsWithContents(filename string) bool { return info.Size() > 4 // at least 4 bytes } +// GetEnvWithContext retrieves the environment variable key. If it does not exist it returns the default. +// The context may optionally contain a map superseding os.EnvKey. +func GetEnvWithContext(ctx context.Context, key string, dfault string, combineWith ...string) string { + var value string + if env, ok := ctx.Value(common.EnvKey).(common.EnvMap); ok { + value = env[common.EnvKeyType(key)] + } + if value == "" { + value = os.Getenv(key) + } + if value == "" { + value = dfault + } + + return combine(value, combineWith) +} + // GetEnv retrieves the environment variable key. If it does not exist it returns the default. func GetEnv(key string, dfault string, combineWith ...string) string { value := os.Getenv(key) @@ -327,6 +371,10 @@ func GetEnv(key string, dfault string, combineWith ...string) string { value = dfault } + return combine(value, combineWith) +} + +func combine(value string, combineWith []string) string { switch len(combineWith) { case 0: return value @@ -364,14 +412,40 @@ func HostDev(combineWith ...string) string { return GetEnv("HOST_DEV", "/dev", combineWith...) } -// MockEnv set environment variable and return revert function. -// MockEnv should be used testing only. -func MockEnv(key string, value string) func() { - original := os.Getenv(key) - os.Setenv(key, value) - return func() { - os.Setenv(key, original) - } +func HostRoot(combineWith ...string) string { + return GetEnv("HOST_ROOT", "/", combineWith...) +} + +func HostProcWithContext(ctx context.Context, combineWith ...string) string { + return GetEnvWithContext(ctx, "HOST_PROC", "/proc", combineWith...) +} + +func HostProcMountInfoWithContext(ctx context.Context, combineWith ...string) string { + return GetEnvWithContext(ctx, "HOST_PROC_MOUNTINFO", "", combineWith...) +} + +func HostSysWithContext(ctx context.Context, combineWith ...string) string { + return GetEnvWithContext(ctx, "HOST_SYS", "/sys", combineWith...) +} + +func HostEtcWithContext(ctx context.Context, combineWith ...string) string { + return GetEnvWithContext(ctx, "HOST_ETC", "/etc", combineWith...) +} + +func HostVarWithContext(ctx context.Context, combineWith ...string) string { + return GetEnvWithContext(ctx, "HOST_VAR", "/var", combineWith...) +} + +func HostRunWithContext(ctx context.Context, combineWith ...string) string { + return GetEnvWithContext(ctx, "HOST_RUN", "/run", combineWith...) +} + +func HostDevWithContext(ctx context.Context, combineWith ...string) string { + return GetEnvWithContext(ctx, "HOST_DEV", "/dev", combineWith...) +} + +func HostRootWithContext(ctx context.Context, combineWith ...string) string { + return GetEnvWithContext(ctx, "HOST_ROOT", "/", combineWith...) } // getSysctrlEnv sets LC_ALL=C in a list of env vars for use when running diff --git a/internal/common/common_linux.go b/internal/common/common_linux.go index da44c3f..a644687 100644 --- a/internal/common/common_linux.go +++ b/internal/common/common_linux.go @@ -12,6 +12,7 @@ import ( "strconv" "strings" "sync" + "syscall" "time" ) @@ -30,7 +31,11 @@ func DoSysctrl(mib string) ([]string, error) { } func NumProcs() (uint64, error) { - f, err := os.Open(HostProc()) + return NumProcsWithContext(context.Background()) +} + +func NumProcsWithContext(ctx context.Context) (uint64, error) { + f, err := os.Open(HostProcWithContext(ctx)) if err != nil { return 0, err } @@ -57,50 +62,70 @@ func BootTimeWithContext(ctx context.Context) (uint64, error) { return 0, err } - statFile := "stat" + useStatFile := true if system == "lxc" && role == "guest" { // if lxc, /proc/uptime is used. - statFile = "uptime" + useStatFile = false } else if system == "docker" && role == "guest" { // also docker, guest - statFile = "uptime" + useStatFile = false + } + + if useStatFile { + return readBootTimeStat(ctx) } - filename := HostProc(statFile) + filename := HostProcWithContext(ctx, "uptime") lines, err := ReadLines(filename) if err != nil { + return handleBootTimeFileReadErr(err) + } + if len(lines) != 1 { + return 0, fmt.Errorf("wrong uptime format") + } + f := strings.Fields(lines[0]) + b, err := strconv.ParseFloat(f[0], 64) + if err != nil { return 0, err } + currentTime := float64(time.Now().UnixNano()) / float64(time.Second) + t := currentTime - b + return uint64(t), nil +} - if statFile == "stat" { - for _, line := range lines { - if strings.HasPrefix(line, "btime") { - f := strings.Fields(line) - if len(f) != 2 { - return 0, fmt.Errorf("wrong btime format") - } - b, err := strconv.ParseInt(f[1], 10, 64) - if err != nil { - return 0, err - } - t := uint64(b) - return t, nil - } - } - } else if statFile == "uptime" { - if len(lines) != 1 { - return 0, fmt.Errorf("wrong uptime format") - } - f := strings.Fields(lines[0]) - b, err := strconv.ParseFloat(f[0], 64) +func handleBootTimeFileReadErr(err error) (uint64, error) { + if os.IsPermission(err) { + var info syscall.Sysinfo_t + err := syscall.Sysinfo(&info) if err != nil { return 0, err } - currentTime := float64(time.Now().UnixNano()) / float64(time.Second) - t := currentTime - b + + currentTime := time.Now().UnixNano() / int64(time.Second) + t := currentTime - int64(info.Uptime) return uint64(t), nil } + return 0, err +} +func readBootTimeStat(ctx context.Context) (uint64, error) { + filename := HostProcWithContext(ctx, "stat") + line, err := ReadLine(filename, "btime") + if err != nil { + return handleBootTimeFileReadErr(err) + } + if strings.HasPrefix(line, "btime") { + f := strings.Fields(line) + if len(f) != 2 { + return 0, fmt.Errorf("wrong btime format") + } + b, err := strconv.ParseInt(f[1], 10, 64) + if err != nil { + return 0, err + } + t := uint64(b) + return t, nil + } return 0, fmt.Errorf("could not find btime") } @@ -127,7 +152,7 @@ func VirtualizationWithContext(ctx context.Context) (string, string, error) { } cachedVirtMutex.RUnlock() - filename := HostProc("xen") + filename := HostProcWithContext(ctx, "xen") if PathExists(filename) { system = "xen" role = "guest" // assume guest @@ -142,13 +167,16 @@ func VirtualizationWithContext(ctx context.Context) (string, string, error) { } } - filename = HostProc("modules") + filename = HostProcWithContext(ctx, "modules") if PathExists(filename) { contents, err := ReadLines(filename) if err == nil { if StringsContains(contents, "kvm") { system = "kvm" role = "host" + } else if StringsContains(contents, "hv_util") { + system = "hyperv" + role = "guest" } else if StringsContains(contents, "vboxdrv") { system = "vbox" role = "host" @@ -162,7 +190,7 @@ func VirtualizationWithContext(ctx context.Context) (string, string, error) { } } - filename = HostProc("cpuinfo") + filename = HostProcWithContext(ctx, "cpuinfo") if PathExists(filename) { contents, err := ReadLines(filename) if err == nil { @@ -175,7 +203,7 @@ func VirtualizationWithContext(ctx context.Context) (string, string, error) { } } - filename = HostProc("bus/pci/devices") + filename = HostProcWithContext(ctx, "bus/pci/devices") if PathExists(filename) { contents, err := ReadLines(filename) if err == nil { @@ -185,7 +213,7 @@ func VirtualizationWithContext(ctx context.Context) (string, string, error) { } } - filename = HostProc() + filename = HostProcWithContext(ctx) if PathExists(filepath.Join(filename, "bc", "0")) { system = "openvz" role = "host" @@ -236,14 +264,19 @@ func VirtualizationWithContext(ctx context.Context) (string, string, error) { } } - if PathExists(HostEtc("os-release")) { - p, _, err := GetOSRelease() + if PathExists(HostEtcWithContext(ctx, "os-release")) { + p, _, err := GetOSReleaseWithContext(ctx) if err == nil && p == "coreos" { system = "rkt" // Is it true? role = "host" } } + if PathExists(HostRootWithContext(ctx, ".dockerenv")) { + system = "docker" + role = "guest" + } + // before returning for the first time, cache the system and role cachedVirtOnce.Do(func() { cachedVirtMutex.Lock() @@ -258,7 +291,11 @@ func VirtualizationWithContext(ctx context.Context) (string, string, error) { } func GetOSRelease() (platform string, version string, err error) { - contents, err := ReadLines(HostEtc("os-release")) + return GetOSReleaseWithContext(context.Background()) +} + +func GetOSReleaseWithContext(ctx context.Context) (platform string, version string, err error) { + contents, err := ReadLines(HostEtcWithContext(ctx, "os-release")) if err != nil { return "", "", nil // return empty } diff --git a/internal/common/common_test.go b/internal/common/common_test.go index 27bcfac..424ea26 100644 --- a/internal/common/common_test.go +++ b/internal/common/common_test.go @@ -1,12 +1,15 @@ package common import ( + "context" "fmt" "os" "reflect" "runtime" "strings" "testing" + + "github.com/shirou/gopsutil/v3/common" ) func TestReadlines(t *testing.T) { @@ -125,7 +128,7 @@ func TestHostEtc(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("windows doesn't have etc") } - p := HostEtc("mtab") + p := HostEtcWithContext(context.Background(), "mtab") if p != "/etc/mtab" { t.Errorf("invalid HostEtc, %s", p) } @@ -160,3 +163,36 @@ func TestGetSysctrlEnv(t *testing.T) { t.Errorf("unexpected real result from getSysctrlEnv: %q", env) } } + +func TestGetEnvDefault(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("windows doesn't have etc") + } + p := HostEtcWithContext(context.Background(), "mtab") + if p != "/etc/mtab" { + t.Errorf("invalid HostEtc, %s", p) + } +} + +func TestGetEnvWithNoContext(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("windows doesn't have etc") + } + t.Setenv("HOST_ETC", "/bar") + p := HostEtcWithContext(context.Background(), "mtab") + if p != "/bar/mtab" { + t.Errorf("invalid HostEtc, %s", p) + } +} + +func TestGetEnvWithContextOverride(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("windows doesn't have etc") + } + t.Setenv("HOST_ETC", "/bar") + ctx := context.WithValue(context.Background(), common.EnvKey, common.EnvMap{common.HostEtcEnvKey: "/foo"}) + p := HostEtcWithContext(ctx, "mtab") + if p != "/foo/mtab" { + t.Errorf("invalid HostEtc, %s", p) + } +} diff --git a/internal/common/common_windows.go b/internal/common/common_windows.go index c98e59c..0e5e4eb 100644 --- a/internal/common/common_windows.go +++ b/internal/common/common_windows.go @@ -218,9 +218,12 @@ func WMIQueryWithContext(ctx context.Context, query string, dst interface{}, con } // Convert paths using native DOS format like: -// "\Device\HarddiskVolume1\Windows\systemew\file.txt" +// +// "\Device\HarddiskVolume1\Windows\systemew\file.txt" +// // into: -// "C:\Windows\systemew\file.txt" +// +// "C:\Windows\systemew\file.txt" func ConvertDOSPath(p string) string { rawDrive := strings.Join(strings.Split(p, `\`)[:3], `\`) diff --git a/internal/common/sleep.go b/internal/common/sleep.go index 8c35b17..9bed241 100644 --- a/internal/common/sleep.go +++ b/internal/common/sleep.go @@ -11,6 +11,9 @@ func Sleep(ctx context.Context, interval time.Duration) error { timer := time.NewTimer(interval) select { case <-ctx.Done(): + if !timer.Stop() { + <-timer.C + } return ctx.Err() case <-timer.C: return nil diff --git a/host/types.go b/internal/common/warnings.go similarity index 59% rename from host/types.go rename to internal/common/warnings.go index c2e7c0b..a4aaada 100644 --- a/host/types.go +++ b/internal/common/warnings.go @@ -1,11 +1,10 @@ -package host +package common -import ( - "fmt" -) +import "fmt" type Warnings struct { - List []error + List []error + Verbose bool } func (w *Warnings) Add(err error) { @@ -20,5 +19,12 @@ func (w *Warnings) Reference() error { } func (w *Warnings) Error() string { + if w.Verbose { + str := "" + for i, e := range w.List { + str += fmt.Sprintf("\tError %d: %s\n", i, e.Error()) + } + return str + } return fmt.Sprintf("Number of warnings: %v", len(w.List)) } diff --git a/load/load_aix_nocgo.go b/load/load_aix_nocgo.go index bfc4850..25d50ac 100644 --- a/load/load_aix_nocgo.go +++ b/load/load_aix_nocgo.go @@ -53,13 +53,13 @@ func MiscWithContext(ctx context.Context) (*MiscStat, error) { for _, line := range strings.Split(string(out), "\n") { ret.ProcsTotal++ switch line { - case "R": - case "A": - ret.ProcsRunning++ - case "T": - ret.ProcsBlocked++ - default: - continue + case "R": + case "A": + ret.ProcsRunning++ + case "T": + ret.ProcsBlocked++ + default: + continue } } return ret, nil diff --git a/load/load_linux.go b/load/load_linux.go index debf073..0298c8b 100644 --- a/load/load_linux.go +++ b/load/load_linux.go @@ -18,7 +18,7 @@ func Avg() (*AvgStat, error) { } func AvgWithContext(ctx context.Context) (*AvgStat, error) { - stat, err := fileAvgWithContext() + stat, err := fileAvgWithContext(ctx) if err != nil { stat, err = sysinfoAvgWithContext() } @@ -40,8 +40,8 @@ func sysinfoAvgWithContext() (*AvgStat, error) { }, nil } -func fileAvgWithContext() (*AvgStat, error) { - values, err := readLoadAvgFromFile() +func fileAvgWithContext(ctx context.Context) (*AvgStat, error) { + values, err := readLoadAvgFromFile(ctx) if err != nil { return nil, err } @@ -75,7 +75,7 @@ func Misc() (*MiscStat, error) { } func MiscWithContext(ctx context.Context) (*MiscStat, error) { - filename := common.HostProc("stat") + filename := common.HostProcWithContext(ctx, "stat") out, err := ioutil.ReadFile(filename) if err != nil { return nil, err @@ -107,7 +107,7 @@ func MiscWithContext(ctx context.Context) (*MiscStat, error) { } - procsTotal, err := getProcsTotal() + procsTotal, err := getProcsTotal(ctx) if err != nil { return ret, err } @@ -116,16 +116,16 @@ func MiscWithContext(ctx context.Context) (*MiscStat, error) { return ret, nil } -func getProcsTotal() (int64, error) { - values, err := readLoadAvgFromFile() +func getProcsTotal(ctx context.Context) (int64, error) { + values, err := readLoadAvgFromFile(ctx) if err != nil { return 0, err } return strconv.ParseInt(strings.Split(values[3], "/")[1], 10, 64) } -func readLoadAvgFromFile() ([]string, error) { - loadavgFilename := common.HostProc("loadavg") +func readLoadAvgFromFile(ctx context.Context) ([]string, error) { + loadavgFilename := common.HostProcWithContext(ctx, "loadavg") line, err := ioutil.ReadFile(loadavgFilename) if err != nil { return nil, err diff --git a/load/load_windows.go b/load/load_windows.go index e1dec29..005ef43 100644 --- a/load/load_windows.go +++ b/load/load_windows.go @@ -26,7 +26,7 @@ var ( // TODO instead of this goroutine, we can register a Win32 counter just as psutil does // see https://psutil.readthedocs.io/en/latest/#psutil.getloadavg // code https://github.com/giampaolo/psutil/blob/8415355c8badc9c94418b19bdf26e622f06f0cce/psutil/arch/windows/wmi.c -func loadAvgGoroutine() { +func loadAvgGoroutine(ctx context.Context) { var ( samplingFrequency time.Duration = 5 * time.Second loadAvgFactor1M float64 = 1 / math.Exp(samplingFrequency.Seconds()/time.Minute.Seconds()) @@ -46,12 +46,20 @@ func loadAvgGoroutine() { } loadAvgMutex.Lock() - loadErr = err loadAvg1M = loadAvg1M*loadAvgFactor1M + currentLoad*(1-loadAvgFactor1M) loadAvg5M = loadAvg5M*loadAvgFactor5M + currentLoad*(1-loadAvgFactor5M) loadAvg15M = loadAvg15M*loadAvgFactor15M + currentLoad*(1-loadAvgFactor15M) loadAvgMutex.Unlock() - <-tick + } + + f() // run first time + for { + select { + case <-ctx.Done(): + return + case <-tick: + f() + } } } @@ -62,7 +70,7 @@ func Avg() (*AvgStat, error) { func AvgWithContext(ctx context.Context) (*AvgStat, error) { loadAvgGoroutineOnce.Do(func() { - go loadAvgGoroutine() + go loadAvgGoroutine(ctx) }) loadAvgMutex.RLock() defer loadAvgMutex.RUnlock() diff --git a/mem/mem_aix_nocgo.go b/mem/mem_aix_nocgo.go index 09ffd8e..fc9e492 100644 --- a/mem/mem_aix_nocgo.go +++ b/mem/mem_aix_nocgo.go @@ -71,7 +71,7 @@ func callSVMon(ctx context.Context) (*VirtualMemoryStat, *SwapMemoryStat, error) swap.Total = t * pagesize } if t, err := strconv.ParseUint(p[3], 10, 64); err == nil { - swap.Free = swap.Total - t * pagesize + swap.Free = swap.Total - t*pagesize } } break diff --git a/mem/mem_darwin.go b/mem/mem_darwin.go index 0527dd9..a05a0fa 100644 --- a/mem/mem_darwin.go +++ b/mem/mem_darwin.go @@ -8,8 +8,9 @@ import ( "fmt" "unsafe" - "github.com/shirou/gopsutil/v3/internal/common" "golang.org/x/sys/unix" + + "github.com/shirou/gopsutil/v3/internal/common" ) func getHwMemsize() (uint64, error) { diff --git a/mem/mem_freebsd.go b/mem/mem_freebsd.go index 44543ef..9a56785 100644 --- a/mem/mem_freebsd.go +++ b/mem/mem_freebsd.go @@ -8,9 +8,8 @@ import ( "errors" "unsafe" - "golang.org/x/sys/unix" - "github.com/shirou/gopsutil/v3/internal/common" + "golang.org/x/sys/unix" ) func VirtualMemory() (*VirtualMemoryStat, error) { diff --git a/mem/mem_linux.go b/mem/mem_linux.go index 9a5d693..9353317 100644 --- a/mem/mem_linux.go +++ b/mem/mem_linux.go @@ -14,8 +14,9 @@ import ( "strconv" "strings" - "github.com/shirou/gopsutil/v3/internal/common" "golang.org/x/sys/unix" + + "github.com/shirou/gopsutil/v3/internal/common" ) type VirtualMemoryExStat struct { @@ -36,7 +37,7 @@ func VirtualMemory() (*VirtualMemoryStat, error) { } func VirtualMemoryWithContext(ctx context.Context) (*VirtualMemoryStat, error) { - vm, _, err := fillFromMeminfoWithContext() + vm, _, err := fillFromMeminfoWithContext(ctx) if err != nil { return nil, err } @@ -48,15 +49,15 @@ func VirtualMemoryEx() (*VirtualMemoryExStat, error) { } func VirtualMemoryExWithContext(ctx context.Context) (*VirtualMemoryExStat, error) { - _, vmEx, err := fillFromMeminfoWithContext() + _, vmEx, err := fillFromMeminfoWithContext(ctx) if err != nil { return nil, err } return vmEx, nil } -func fillFromMeminfoWithContext() (*VirtualMemoryStat, *VirtualMemoryExStat, error) { - filename := common.HostProc("meminfo") +func fillFromMeminfoWithContext(ctx context.Context) (*VirtualMemoryStat, *VirtualMemoryExStat, error) { + filename := common.HostProcWithContext(ctx, "meminfo") lines, _ := common.ReadLines(filename) // flag if MemAvailable is in /proc/meminfo (kernel 3.14+) @@ -153,13 +154,13 @@ func fillFromMeminfoWithContext() (*VirtualMemoryStat, *VirtualMemoryExStat, err return ret, retEx, err } retEx.Unevictable = t * 1024 - case "WriteBack": + case "Writeback": t, err := strconv.ParseUint(value, 10, 64) if err != nil { return ret, retEx, err } ret.WriteBack = t * 1024 - case "WriteBackTmp": + case "WritebackTmp": t, err := strconv.ParseUint(value, 10, 64) if err != nil { return ret, retEx, err @@ -317,7 +318,7 @@ func fillFromMeminfoWithContext() (*VirtualMemoryStat, *VirtualMemoryExStat, err if !memavail { if activeFile && inactiveFile && sReclaimable { - ret.Available = calculateAvailVmem(ret, retEx) + ret.Available = calculateAvailVmem(ctx, ret, retEx) } else { ret.Available = ret.Cached + ret.Free } @@ -350,7 +351,7 @@ func SwapMemoryWithContext(ctx context.Context) (*SwapMemoryStat, error) { } else { ret.UsedPercent = 0 } - filename := common.HostProc("vmstat") + filename := common.HostProcWithContext(ctx, "vmstat") lines, _ := common.ReadLines(filename) for _, l := range lines { fields := strings.Fields(l) @@ -370,25 +371,25 @@ func SwapMemoryWithContext(ctx context.Context) (*SwapMemoryStat, error) { continue } ret.Sout = value * 4 * 1024 - case "pgpgIn": + case "pgpgin": value, err := strconv.ParseUint(fields[1], 10, 64) if err != nil { continue } ret.PgIn = value * 4 * 1024 - case "pgpgOut": + case "pgpgout": value, err := strconv.ParseUint(fields[1], 10, 64) if err != nil { continue } ret.PgOut = value * 4 * 1024 - case "pgFault": + case "pgfault": value, err := strconv.ParseUint(fields[1], 10, 64) if err != nil { continue } ret.PgFault = value * 4 * 1024 - case "pgMajFault": + case "pgmajfault": value, err := strconv.ParseUint(fields[1], 10, 64) if err != nil { continue @@ -402,10 +403,10 @@ func SwapMemoryWithContext(ctx context.Context) (*SwapMemoryStat, error) { // calculateAvailVmem is a fallback under kernel 3.14 where /proc/meminfo does not provide // "MemAvailable:" column. It reimplements an algorithm from the link below // https://github.com/giampaolo/psutil/pull/890 -func calculateAvailVmem(ret *VirtualMemoryStat, retEx *VirtualMemoryExStat) uint64 { +func calculateAvailVmem(ctx context.Context, ret *VirtualMemoryStat, retEx *VirtualMemoryExStat) uint64 { var watermarkLow uint64 - fn := common.HostProc("zoneinfo") + fn := common.HostProcWithContext(ctx, "zoneinfo") lines, err := common.ReadLines(fn) if err != nil { return ret.Free + ret.Cached // fallback under kernel 2.6.13 @@ -457,18 +458,18 @@ func SwapDevices() ([]*SwapDevice, error) { } func SwapDevicesWithContext(ctx context.Context) ([]*SwapDevice, error) { - swapsFilePath := common.HostProc(swapsFilename) + swapsFilePath := common.HostProcWithContext(ctx, swapsFilename) f, err := os.Open(swapsFilePath) if err != nil { return nil, err } defer f.Close() - return parseSwapsFile(f) + return parseSwapsFile(ctx, f) } -func parseSwapsFile(r io.Reader) ([]*SwapDevice, error) { - swapsFilePath := common.HostProc(swapsFilename) +func parseSwapsFile(ctx context.Context, r io.Reader) ([]*SwapDevice, error) { + swapsFilePath := common.HostProcWithContext(ctx, swapsFilename) scanner := bufio.NewScanner(r) if !scanner.Scan() { if err := scanner.Err(); err != nil { diff --git a/mem/mem_linux_test.go b/mem/mem_linux_test.go index 61e16a1..d830fbf 100644 --- a/mem/mem_linux_test.go +++ b/mem/mem_linux_test.go @@ -4,7 +4,7 @@ package mem import ( - "os" + "context" "path/filepath" "reflect" "strings" @@ -111,12 +111,9 @@ var virtualMemoryTests = []struct { } func TestVirtualMemoryLinux(t *testing.T) { - origProc := os.Getenv("HOST_PROC") - defer os.Setenv("HOST_PROC", origProc) - for _, tt := range virtualMemoryTests { t.Run(tt.mockedRootFS, func(t *testing.T) { - os.Setenv("HOST_PROC", filepath.Join("testdata/linux/virtualmemory/", tt.mockedRootFS, "proc")) + t.Setenv("HOST_PROC", filepath.Join("testdata/linux/virtualmemory/", tt.mockedRootFS, "proc")) stat, err := VirtualMemory() skipIfNotImplementedErr(t, err) @@ -142,7 +139,7 @@ const invalidFile = `INVALID Type Size Used Priority func TestParseSwapsFile_ValidFile(t *testing.T) { assert := assert.New(t) - stats, err := parseSwapsFile(strings.NewReader(validFile)) + stats, err := parseSwapsFile(context.Background(), strings.NewReader(validFile)) assert.NoError(err) assert.Equal(*stats[0], SwapDevice{ @@ -159,11 +156,11 @@ func TestParseSwapsFile_ValidFile(t *testing.T) { } func TestParseSwapsFile_InvalidFile(t *testing.T) { - _, err := parseSwapsFile(strings.NewReader(invalidFile)) + _, err := parseSwapsFile(context.Background(), strings.NewReader(invalidFile)) assert.Error(t, err) } func TestParseSwapsFile_EmptyFile(t *testing.T) { - _, err := parseSwapsFile(strings.NewReader("")) + _, err := parseSwapsFile(context.Background(), strings.NewReader("")) assert.Error(t, err) } diff --git a/mem/mem_openbsd.go b/mem/mem_openbsd.go index 9764492..e37d5ab 100644 --- a/mem/mem_openbsd.go +++ b/mem/mem_openbsd.go @@ -9,6 +9,7 @@ import ( "encoding/binary" "errors" "fmt" + "github.com/shirou/gopsutil/v3/internal/common" "golang.org/x/sys/unix" ) diff --git a/mem/mem_plan9_test.go b/mem/mem_plan9_test.go index b3480ca..1ae353d 100644 --- a/mem/mem_plan9_test.go +++ b/mem/mem_plan9_test.go @@ -4,7 +4,6 @@ package mem import ( - "os" "reflect" "testing" ) @@ -27,14 +26,9 @@ var virtualMemoryTests = []struct { } func TestVirtualMemoryPlan9(t *testing.T) { - origProc := os.Getenv("HOST_ROOT") - t.Cleanup(func() { - os.Setenv("HOST_ROOT", origProc) - }) - for _, tt := range virtualMemoryTests { t.Run(tt.mockedRootFS, func(t *testing.T) { - os.Setenv("HOST_ROOT", "testdata/plan9/virtualmemory/") + t.Setenv("HOST_ROOT", "testdata/plan9/virtualmemory/") stat, err := VirtualMemory() skipIfNotImplementedErr(t, err) @@ -62,14 +56,9 @@ var swapMemoryTests = []struct { } func TestSwapMemoryPlan9(t *testing.T) { - origProc := os.Getenv("HOST_ROOT") - t.Cleanup(func() { - os.Setenv("HOST_ROOT", origProc) - }) - for _, tt := range swapMemoryTests { t.Run(tt.mockedRootFS, func(t *testing.T) { - os.Setenv("HOST_ROOT", "testdata/plan9/virtualmemory/") + t.Setenv("HOST_ROOT", "testdata/plan9/virtualmemory/") swap, err := SwapMemory() skipIfNotImplementedErr(t, err) diff --git a/mem/mem_solaris.go b/mem/mem_solaris.go index 88f05f6..c911267 100644 --- a/mem/mem_solaris.go +++ b/mem/mem_solaris.go @@ -12,6 +12,7 @@ import ( "strings" "github.com/shirou/gopsutil/v3/internal/common" + "github.com/tklauser/go-sysconf" ) // VirtualMemory for Solaris is a minimal implementation which only returns @@ -34,6 +35,13 @@ func VirtualMemoryWithContext(ctx context.Context) (*VirtualMemoryStat, error) { return nil, err } result.Total = cap + freemem, err := globalZoneFreeMemory(ctx) + if err != nil { + return nil, err + } + result.Available = freemem + result.Free = freemem + result.Used = result.Total - result.Free } else { cap, err := nonGlobalZoneMemoryCapacity() if err != nil { @@ -85,6 +93,25 @@ func globalZoneMemoryCapacity() (uint64, error) { return totalMB * 1024 * 1024, nil } +func globalZoneFreeMemory(ctx context.Context) (uint64, error) { + output, err := invoke.CommandWithContext(ctx, "pagesize") + if err != nil { + return 0, err + } + + pagesize, err := strconv.ParseUint(strings.TrimSpace(string(output)), 10, 64) + if err != nil { + return 0, err + } + + free, err := sysconf.Sysconf(sysconf.SC_AVPHYS_PAGES) + if err != nil { + return 0, err + } + + return uint64(free) * pagesize, nil +} + var kstatMatch = regexp.MustCompile(`(\S+)\s+(\S*)`) func nonGlobalZoneMemoryCapacity() (uint64, error) { diff --git a/mem/mem_test.go b/mem/mem_test.go index d469674..3be5656 100644 --- a/mem/mem_test.go +++ b/mem/mem_test.go @@ -6,8 +6,9 @@ import ( "runtime" "testing" - "github.com/shirou/gopsutil/v3/internal/common" "github.com/stretchr/testify/assert" + + "github.com/shirou/gopsutil/v3/internal/common" ) func skipIfNotImplementedErr(t *testing.T, err error) { @@ -17,8 +18,8 @@ func skipIfNotImplementedErr(t *testing.T, err error) { } func TestVirtual_memory(t *testing.T) { - if runtime.GOOS == "solaris" { - t.Skip("Only .Total is supported on Solaris") + if runtime.GOOS == "solaris" || runtime.GOOS == "illumos" { + t.Skip("Only .Total .Available are supported on Solaris/illumos") } v, err := VirtualMemory() diff --git a/net/net_aix_cgo.go b/net/net_aix_cgo.go index 8cf8c91..8c34f88 100644 --- a/net/net_aix_cgo.go +++ b/net/net_aix_cgo.go @@ -18,14 +18,14 @@ func IOCountersWithContext(ctx context.Context, pernic bool) ([]IOCountersStat, iocounters := make([]IOCountersStat, 0, len(ifs)) for _, netif := range ifs { n := IOCountersStat{ - Name: netif.Name, - BytesSent: uint64(netif.OBytes), - BytesRecv: uint64(netif.IBytes), + Name: netif.Name, + BytesSent: uint64(netif.OBytes), + BytesRecv: uint64(netif.IBytes), PacketsSent: uint64(netif.OPackets), PacketsRecv: uint64(netif.IPackets), - Errin: uint64(netif.OErrors), - Errout: uint64(netif.IErrors), - Dropout: uint64(netif.XmitDrops), + Errin: uint64(netif.OErrors), + Errout: uint64(netif.IErrors), + Dropout: uint64(netif.XmitDrops), } iocounters = append(iocounters, n) } diff --git a/net/net_darwin.go b/net/net_darwin.go index 1c8d4f4..8a7b637 100644 --- a/net/net_darwin.go +++ b/net/net_darwin.go @@ -259,7 +259,7 @@ func IOCountersByFile(pernic bool, filename string) ([]IOCountersStat, error) { } func IOCountersByFileWithContext(ctx context.Context, pernic bool, filename string) ([]IOCountersStat, error) { - return IOCounters(pernic) + return IOCountersWithContext(ctx, pernic) } func FilterCounters() ([]FilterStat, error) { @@ -278,7 +278,7 @@ func ConntrackStatsWithContext(ctx context.Context, percpu bool) ([]ConntrackSta return nil, common.ErrNotImplementedError } -// NetProtoCounters returns network statistics for the entire system +// ProtoCounters returns network statistics for the entire system // If protocols is empty then all protocols are returned, otherwise // just the protocols in the list are returned. // Not Implemented for Darwin diff --git a/net/net_fallback.go b/net/net_fallback.go index 58325f6..e136be1 100644 --- a/net/net_fallback.go +++ b/net/net_fallback.go @@ -1,5 +1,5 @@ -//go:build !aix && !darwin && !linux && !freebsd && !openbsd && !windows -// +build !aix,!darwin,!linux,!freebsd,!openbsd,!windows +//go:build !aix && !darwin && !linux && !freebsd && !openbsd && !windows && !solaris +// +build !aix,!darwin,!linux,!freebsd,!openbsd,!windows,!solaris package net diff --git a/net/net_freebsd.go b/net/net_freebsd.go index 7f31851..bf8baf0 100644 --- a/net/net_freebsd.go +++ b/net/net_freebsd.go @@ -115,7 +115,7 @@ func ConntrackStatsWithContext(ctx context.Context, percpu bool) ([]ConntrackSta return nil, common.ErrNotImplementedError } -// NetProtoCounters returns network statistics for the entire system +// ProtoCounters returns network statistics for the entire system // If protocols is empty then all protocols are returned, otherwise // just the protocols in the list are returned. // Not Implemented for FreeBSD diff --git a/net/net_linux.go b/net/net_linux.go index c7cd0db..de0ea73 100644 --- a/net/net_linux.go +++ b/net/net_linux.go @@ -50,7 +50,7 @@ func IOCounters(pernic bool) ([]IOCountersStat, error) { } func IOCountersWithContext(ctx context.Context, pernic bool) ([]IOCountersStat, error) { - filename := common.HostProc("net/dev") + filename := common.HostProcWithContext(ctx, "net/dev") return IOCountersByFileWithContext(ctx, pernic, filename) } @@ -157,7 +157,7 @@ var netProtocols = []string{ "udplite", } -// NetProtoCounters returns network statistics for the entire system +// ProtoCounters returns network statistics for the entire system // If protocols is empty then all protocols are returned, otherwise // just the protocols in the list are returned. // Available protocols: @@ -177,7 +177,7 @@ func ProtoCountersWithContext(ctx context.Context, protocols []string) ([]ProtoC protos[p] = true } - filename := common.HostProc("net/snmp") + filename := common.HostProcWithContext(ctx, "net/snmp") lines, err := common.ReadLines(filename) if err != nil { return nil, err @@ -230,8 +230,8 @@ func FilterCounters() ([]FilterStat, error) { } func FilterCountersWithContext(ctx context.Context) ([]FilterStat, error) { - countfile := common.HostProc("sys/net/netfilter/nf_conntrack_count") - maxfile := common.HostProc("sys/net/netfilter/nf_conntrack_max") + countfile := common.HostProcWithContext(ctx, "sys/net/netfilter/nf_conntrack_count") + maxfile := common.HostProcWithContext(ctx, "sys/net/netfilter/nf_conntrack_max") count, err := common.ReadInts(countfile) if err != nil { @@ -260,7 +260,7 @@ func ConntrackStats(percpu bool) ([]ConntrackStat, error) { // ConntrackStatsWithContext returns more detailed info about the conntrack table func ConntrackStatsWithContext(ctx context.Context, percpu bool) ([]ConntrackStat, error) { - return conntrackStatsFromFile(common.HostProc("net/stat/nf_conntrack"), percpu) + return conntrackStatsFromFile(common.HostProcWithContext(ctx, "net/stat/nf_conntrack"), percpu) } // conntrackStatsFromFile returns more detailed info about the conntrack table @@ -459,7 +459,7 @@ func connectionsPidMaxWithoutUidsWithContext(ctx context.Context, kind string, p if !ok { return nil, fmt.Errorf("invalid kind, %s", kind) } - root := common.HostProc() + root := common.HostProcWithContext(ctx) var err error var inodes map[string][]inodeMap if pid == 0 { @@ -531,7 +531,7 @@ func statsFromInodesWithContext(ctx context.Context, root string, pid int32, tma if !skipUids { // fetch process owner Real, effective, saved set, and filesystem UIDs proc := process{Pid: conn.Pid} - conn.Uids, _ = proc.getUids() + conn.Uids, _ = proc.getUids(ctx) } ret = append(ret, conn) @@ -599,7 +599,7 @@ func Pids() ([]int32, error) { func PidsWithContext(ctx context.Context) ([]int32, error) { var ret []int32 - d, err := os.Open(common.HostProc()) + d, err := os.Open(common.HostProcWithContext(ctx)) if err != nil { return nil, err } @@ -631,8 +631,8 @@ type process struct { } // Uids returns user ids of the process as a slice of the int -func (p *process) getUids() ([]int32, error) { - err := p.fillFromStatus() +func (p *process) getUids(ctx context.Context) ([]int32, error) { + err := p.fillFromStatus(ctx) if err != nil { return []int32{}, err } @@ -640,9 +640,9 @@ func (p *process) getUids() ([]int32, error) { } // Get status from /proc/(pid)/status -func (p *process) fillFromStatus() error { +func (p *process) fillFromStatus(ctx context.Context) error { pid := p.Pid - statPath := common.HostProc(strconv.Itoa(int(pid)), "status") + statPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "status") contents, err := ioutil.ReadFile(statPath) if err != nil { return err diff --git a/net/net_linux_test.go b/net/net_linux_test.go index d5d6252..f1b7fba 100644 --- a/net/net_linux_test.go +++ b/net/net_linux_test.go @@ -1,6 +1,7 @@ package net import ( + "context" "fmt" "io/ioutil" "net" @@ -9,8 +10,9 @@ import ( "syscall" "testing" - "github.com/shirou/gopsutil/v3/internal/common" "github.com/stretchr/testify/assert" + + "github.com/shirou/gopsutil/v3/internal/common" ) func TestIOCountersByFileParsing(t *testing.T) { @@ -100,7 +102,7 @@ func TestGetProcInodesAll(t *testing.T) { }() <-waitForServer - root := common.HostProc("") + root := common.HostProcWithContext(context.Background(), "") v, err := getProcInodesAll(root, 0) assert.Nil(t, err) assert.NotEmpty(t, v) @@ -248,6 +250,7 @@ entries searched found new invalid ignore delete deleteList insert insertFailed // Function under test stats, err := conntrackStatsFromFile(tmpfile.Name(), true) + assert.Nil(t, err) assert.Equal(t, 8, len(stats), "Expected 8 results") summary := &ConntrackStat{} @@ -308,6 +311,7 @@ entries searched found new invalid ignore delete deleteList insert insertFailed // Test summary grouping totals, err := conntrackStatsFromFile(tmpfile.Name(), false) + assert.Nil(t, err) for i, st := range totals { assert.Equal(t, summary.Entries, st.Entries) assert.Equal(t, summary.Searched, st.Searched) diff --git a/net/net_openbsd.go b/net/net_openbsd.go index 5f066a0..cf48f53 100644 --- a/net/net_openbsd.go +++ b/net/net_openbsd.go @@ -164,7 +164,7 @@ func ConntrackStatsWithContext(ctx context.Context, percpu bool) ([]ConntrackSta return nil, common.ErrNotImplementedError } -// NetProtoCounters returns network statistics for the entire system +// ProtoCounters returns network statistics for the entire system // If protocols is empty then all protocols are returned, otherwise // just the protocols in the list are returned. // Not Implemented for OpenBSD diff --git a/net/net_solaris.go b/net/net_solaris.go new file mode 100644 index 0000000..7f1f5c8 --- /dev/null +++ b/net/net_solaris.go @@ -0,0 +1,143 @@ +//go:build solaris +// +build solaris + +package net + +import ( + "context" + "fmt" + "regexp" + "runtime" + "strconv" + "strings" + + "github.com/shirou/gopsutil/v3/internal/common" +) + +// NetIOCounters returnes network I/O statistics for every network +// interface installed on the system. If pernic argument is false, +// return only sum of all information (which name is 'all'). If true, +// every network interface installed on the system is returned +// separately. +func IOCounters(pernic bool) ([]IOCountersStat, error) { + return IOCountersWithContext(context.Background(), pernic) +} + +func IOCountersWithContext(ctx context.Context, pernic bool) ([]IOCountersStat, error) { + // collect all the net class's links with below statistics + filterstr := "/^(?!vnic)/::phys:/^rbytes64$|^ipackets64$|^idrops64$|^ierrors$|^obytes64$|^opackets64$|^odrops64$|^oerrors$/" + if runtime.GOOS == "illumos" { + filterstr = "/[^vnic]/::mac:/^rbytes64$|^ipackets64$|^idrops64$|^ierrors$|^obytes64$|^opackets64$|^odrops64$|^oerrors$/" + } + kstatSysOut, err := invoke.CommandWithContext(ctx, "kstat", "-c", "net", "-p", filterstr) + if err != nil { + return nil, fmt.Errorf("cannot execute kstat: %w", err) + } + + lines := strings.Split(strings.TrimSpace(string(kstatSysOut)), "\n") + if len(lines) == 0 { + return nil, fmt.Errorf("no interface found") + } + rbytes64arr := make(map[string]uint64) + ipackets64arr := make(map[string]uint64) + idrops64arr := make(map[string]uint64) + ierrorsarr := make(map[string]uint64) + obytes64arr := make(map[string]uint64) + opackets64arr := make(map[string]uint64) + odrops64arr := make(map[string]uint64) + oerrorsarr := make(map[string]uint64) + + re := regexp.MustCompile(`[:\s]+`) + for _, line := range lines { + fields := re.Split(line, -1) + interfaceName := fields[0] + instance := fields[1] + switch fields[3] { + case "rbytes64": + rbytes64arr[interfaceName+instance], err = strconv.ParseUint(fields[4], 10, 64) + if err != nil { + return nil, fmt.Errorf("cannot parse rbytes64: %w", err) + } + case "ipackets64": + ipackets64arr[interfaceName+instance], err = strconv.ParseUint(fields[4], 10, 64) + if err != nil { + return nil, fmt.Errorf("cannot parse ipackets64: %w", err) + } + case "idrops64": + idrops64arr[interfaceName+instance], err = strconv.ParseUint(fields[4], 10, 64) + if err != nil { + return nil, fmt.Errorf("cannot parse idrops64: %w", err) + } + case "ierrors": + ierrorsarr[interfaceName+instance], err = strconv.ParseUint(fields[4], 10, 64) + if err != nil { + return nil, fmt.Errorf("cannot parse ierrors: %w", err) + } + case "obytes64": + obytes64arr[interfaceName+instance], err = strconv.ParseUint(fields[4], 10, 64) + if err != nil { + return nil, fmt.Errorf("cannot parse obytes64: %w", err) + } + case "opackets64": + opackets64arr[interfaceName+instance], err = strconv.ParseUint(fields[4], 10, 64) + if err != nil { + return nil, fmt.Errorf("cannot parse opackets64: %w", err) + } + case "odrops64": + odrops64arr[interfaceName+instance], err = strconv.ParseUint(fields[4], 10, 64) + if err != nil { + return nil, fmt.Errorf("cannot parse odrops64: %w", err) + } + case "oerrors": + oerrorsarr[interfaceName+instance], err = strconv.ParseUint(fields[4], 10, 64) + if err != nil { + return nil, fmt.Errorf("cannot parse oerrors: %w", err) + } + } + } + ret := make([]IOCountersStat, 0) + for k := range rbytes64arr { + nic := IOCountersStat{ + Name: k, + BytesRecv: rbytes64arr[k], + PacketsRecv: ipackets64arr[k], + Errin: ierrorsarr[k], + Dropin: idrops64arr[k], + BytesSent: obytes64arr[k], + PacketsSent: opackets64arr[k], + Errout: oerrorsarr[k], + Dropout: odrops64arr[k], + } + ret = append(ret, nic) + } + + if !pernic { + return getIOCountersAll(ret) + } + + return ret, nil +} + +func Connections(kind string) ([]ConnectionStat, error) { + return ConnectionsWithContext(context.Background(), kind) +} + +func ConnectionsWithContext(ctx context.Context, kind string) ([]ConnectionStat, error) { + return []ConnectionStat{}, common.ErrNotImplementedError +} + +func FilterCounters() ([]FilterStat, error) { + return FilterCountersWithContext(context.Background()) +} + +func FilterCountersWithContext(ctx context.Context) ([]FilterStat, error) { + return []FilterStat{}, common.ErrNotImplementedError +} + +func ProtoCounters(protocols []string) ([]ProtoCountersStat, error) { + return ProtoCountersWithContext(context.Background(), protocols) +} + +func ProtoCountersWithContext(ctx context.Context, protocols []string) ([]ProtoCountersStat, error) { + return []ProtoCountersStat{}, common.ErrNotImplementedError +} diff --git a/net/net_test.go b/net/net_test.go index bacca04..72f4db9 100644 --- a/net/net_test.go +++ b/net/net_test.go @@ -3,7 +3,6 @@ package net import ( "errors" "fmt" - "math" "os" "runtime" "testing" @@ -86,8 +85,14 @@ func TestNetIOCountersAll(t *testing.T) { for _, p := range per { pr += p.PacketsRecv } - // small diff is ok - if math.Abs(float64(v[0].PacketsRecv-pr)) > 5 { + // small diff is ok, compare instead of math.Abs(subtraction) with uint64 + var diff uint64 + if v[0].PacketsRecv > pr { + diff = v[0].PacketsRecv - pr + } else { + diff = pr - v[0].PacketsRecv + } + if diff > 5 { if ci := os.Getenv("CI"); ci != "" { // This test often fails in CI. so just print even if failed. fmt.Printf("invalid sum value: %v, %v", v[0].PacketsRecv, pr) diff --git a/net/net_unix.go b/net/net_unix.go index 2fd2224..cb846e2 100644 --- a/net/net_unix.go +++ b/net/net_unix.go @@ -20,7 +20,7 @@ func Connections(kind string) ([]ConnectionStat, error) { } func ConnectionsWithContext(ctx context.Context, kind string) ([]ConnectionStat, error) { - return ConnectionsPid(kind, 0) + return ConnectionsPidWithContext(ctx, kind, 0) } // Return a list of network connections opened returning at most `max` diff --git a/net/net_windows.go b/net/net_windows.go index 731c8f9..5d38434 100644 --- a/net/net_windows.go +++ b/net/net_windows.go @@ -211,7 +211,8 @@ func IOCountersByFileWithContext(ctx context.Context, pernic bool, filename stri // Return a list of network connections // Available kind: -// reference to netConnectionKindMap +// +// reference to netConnectionKindMap func Connections(kind string) ([]ConnectionStat, error) { return ConnectionsWithContext(context.Background(), kind) } @@ -337,7 +338,7 @@ func ConntrackStatsWithContext(ctx context.Context, percpu bool) ([]ConntrackSta return nil, common.ErrNotImplementedError } -// NetProtoCounters returns network statistics for the entire system +// ProtoCounters returns network statistics for the entire system // If protocols is empty then all protocols are returned, otherwise // just the protocols in the list are returned. // Not Implemented for Windows diff --git a/process/process.go b/process/process.go index 0ca26c2..1a7fe1b 100644 --- a/process/process.go +++ b/process/process.go @@ -335,7 +335,7 @@ func (p *Process) MemoryPercentWithContext(ctx context.Context) (float32, error) return (100 * float32(used) / float32(total)), nil } -// CPU_Percent returns how many percent of the CPU time this process uses +// CPUPercent returns how many percent of the CPU time this process uses func (p *Process) CPUPercent() (float64, error) { return p.CPUPercentWithContext(context.Background()) } @@ -507,7 +507,7 @@ func (p *Process) MemoryInfoEx() (*MemoryInfoExStat, error) { return p.MemoryInfoExWithContext(context.Background()) } -// PageFaultsInfo returns the process's page fault counters. +// PageFaults returns the process's page fault counters. func (p *Process) PageFaults() (*PageFaultsStat, error) { return p.PageFaultsWithContext(context.Background()) } @@ -530,7 +530,7 @@ func (p *Process) Connections() ([]net.ConnectionStat, error) { return p.ConnectionsWithContext(context.Background()) } -// Connections returns a slice of net.ConnectionStat used by the process at most `max`. +// ConnectionsMax returns a slice of net.ConnectionStat used by the process at most `max`. func (p *Process) ConnectionsMax(max int) ([]net.ConnectionStat, error) { return p.ConnectionsMaxWithContext(context.Background(), max) } diff --git a/process/process_darwin.go b/process/process_darwin.go index 61b340b..176661c 100644 --- a/process/process_darwin.go +++ b/process/process_darwin.go @@ -10,10 +10,11 @@ import ( "strconv" "strings" - "github.com/shirou/gopsutil/v3/internal/common" - "github.com/shirou/gopsutil/v3/net" "github.com/tklauser/go-sysconf" "golang.org/x/sys/unix" + + "github.com/shirou/gopsutil/v3/internal/common" + "github.com/shirou/gopsutil/v3/net" ) // copied from sys/sysctl.h @@ -81,8 +82,6 @@ func (p *Process) NameWithContext(ctx context.Context) (string, error) { extendedName := filepath.Base(cmdName) if strings.HasPrefix(extendedName, p.name) { name = extendedName - } else { - name = cmdName } } } diff --git a/process/process_darwin_cgo.go b/process/process_darwin_cgo.go index 2ac413f..858f08e 100644 --- a/process/process_darwin_cgo.go +++ b/process/process_darwin_cgo.go @@ -175,6 +175,7 @@ func (p *Process) CmdlineWithContext(ctx context.Context) (string, error) { func (p *Process) NumThreadsWithContext(ctx context.Context) (int32, error) { const tiSize = C.sizeof_struct_proc_taskinfo ti := (*C.struct_proc_taskinfo)(C.malloc(tiSize)) + defer C.free(unsafe.Pointer(ti)) _, err := C.proc_pidinfo(C.int(p.Pid), C.PROC_PIDTASKINFO, 0, unsafe.Pointer(ti), tiSize) if err != nil { @@ -187,6 +188,7 @@ func (p *Process) NumThreadsWithContext(ctx context.Context) (int32, error) { func (p *Process) TimesWithContext(ctx context.Context) (*cpu.TimesStat, error) { const tiSize = C.sizeof_struct_proc_taskinfo ti := (*C.struct_proc_taskinfo)(C.malloc(tiSize)) + defer C.free(unsafe.Pointer(ti)) _, err := C.proc_pidinfo(C.int(p.Pid), C.PROC_PIDTASKINFO, 0, unsafe.Pointer(ti), tiSize) if err != nil { @@ -204,6 +206,7 @@ func (p *Process) TimesWithContext(ctx context.Context) (*cpu.TimesStat, error) func (p *Process) MemoryInfoWithContext(ctx context.Context) (*MemoryInfoStat, error) { const tiSize = C.sizeof_struct_proc_taskinfo ti := (*C.struct_proc_taskinfo)(C.malloc(tiSize)) + defer C.free(unsafe.Pointer(ti)) _, err := C.proc_pidinfo(C.int(p.Pid), C.PROC_PIDTASKINFO, 0, unsafe.Pointer(ti), tiSize) if err != nil { diff --git a/process/process_freebsd.go b/process/process_freebsd.go index 779f812..85134b7 100644 --- a/process/process_freebsd.go +++ b/process/process_freebsd.go @@ -55,8 +55,6 @@ func (p *Process) NameWithContext(ctx context.Context) (string, error) { extendedName := filepath.Base(cmdlineSlice[0]) if strings.HasPrefix(extendedName, p.name) { name = extendedName - } else { - name = cmdlineSlice[0] } } } @@ -69,7 +67,13 @@ func (p *Process) CwdWithContext(ctx context.Context) (string, error) { } func (p *Process) ExeWithContext(ctx context.Context) (string, error) { - return "", common.ErrNotImplementedError + mib := []int32{CTLKern, KernProc, KernProcPathname, p.Pid} + buf, _, err := common.CallSyscall(mib) + if err != nil { + return "", err + } + + return strings.Trim(string(buf), "\x00"), nil } func (p *Process) CmdlineWithContext(ctx context.Context) (string, error) { diff --git a/process/process_linux.go b/process/process_linux.go index d5b5bc3..37cb7ca 100644 --- a/process/process_linux.go +++ b/process/process_linux.go @@ -16,11 +16,12 @@ import ( "strconv" "strings" + "github.com/tklauser/go-sysconf" + "golang.org/x/sys/unix" + "github.com/shirou/gopsutil/v3/cpu" "github.com/shirou/gopsutil/v3/internal/common" "github.com/shirou/gopsutil/v3/net" - "github.com/tklauser/go-sysconf" - "golang.org/x/sys/unix" ) var pageSize = uint64(os.Getpagesize()) @@ -100,7 +101,7 @@ func (p *Process) TgidWithContext(ctx context.Context) (int32, error) { } func (p *Process) ExeWithContext(ctx context.Context) (string, error) { - return p.fillFromExeWithContext() + return p.fillFromExeWithContext(ctx) } func (p *Process) CmdlineWithContext(ctx context.Context) (string, error) { @@ -120,7 +121,7 @@ func (p *Process) createTimeWithContext(ctx context.Context) (int64, error) { } func (p *Process) CwdWithContext(ctx context.Context) (string, error) { - return p.fillFromCwdWithContext() + return p.fillFromCwdWithContext(ctx) } func (p *Process) StatusWithContext(ctx context.Context) ([]string, error) { @@ -134,7 +135,7 @@ func (p *Process) StatusWithContext(ctx context.Context) ([]string, error) { func (p *Process) ForegroundWithContext(ctx context.Context) (bool, error) { // see https://github.com/shirou/gopsutil/issues/596#issuecomment-432707831 for implementation details pid := p.Pid - statPath := common.HostProc(strconv.Itoa(int(pid)), "stat") + statPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "stat") contents, err := ioutil.ReadFile(statPath) if err != nil { return false, err @@ -202,7 +203,7 @@ func (p *Process) RlimitWithContext(ctx context.Context) ([]RlimitStat, error) { } func (p *Process) RlimitUsageWithContext(ctx context.Context, gatherUsed bool) ([]RlimitStat, error) { - rlimits, err := p.fillFromLimitsWithContext() + rlimits, err := p.fillFromLimitsWithContext(ctx) if !gatherUsed || err != nil { return rlimits, err } @@ -257,7 +258,7 @@ func (p *Process) RlimitUsageWithContext(ctx context.Context, gatherUsed bool) ( } func (p *Process) IOCountersWithContext(ctx context.Context) (*IOCountersStat, error) { - return p.fillFromIOWithContext() + return p.fillFromIOWithContext(ctx) } func (p *Process) NumCtxSwitchesWithContext(ctx context.Context) (*NumCtxSwitchesStat, error) { @@ -283,7 +284,7 @@ func (p *Process) NumThreadsWithContext(ctx context.Context) (int32, error) { func (p *Process) ThreadsWithContext(ctx context.Context) (map[int32]*cpu.TimesStat, error) { ret := make(map[int32]*cpu.TimesStat) - taskPath := common.HostProc(strconv.Itoa(int(p.Pid)), "task") + taskPath := common.HostProcWithContext(ctx, strconv.Itoa(int(p.Pid)), "task") tids, err := readPidsFromDir(taskPath) if err != nil { @@ -314,7 +315,7 @@ func (p *Process) CPUAffinityWithContext(ctx context.Context) ([]int32, error) { } func (p *Process) MemoryInfoWithContext(ctx context.Context) (*MemoryInfoStat, error) { - meminfo, _, err := p.fillFromStatmWithContext() + meminfo, _, err := p.fillFromStatmWithContext(ctx) if err != nil { return nil, err } @@ -322,7 +323,7 @@ func (p *Process) MemoryInfoWithContext(ctx context.Context) (*MemoryInfoStat, e } func (p *Process) MemoryInfoExWithContext(ctx context.Context) (*MemoryInfoExStat, error) { - _, memInfoEx, err := p.fillFromStatmWithContext() + _, memInfoEx, err := p.fillFromStatmWithContext(ctx) if err != nil { return nil, err } @@ -380,12 +381,12 @@ func (p *Process) ConnectionsMaxWithContext(ctx context.Context, max int) ([]net func (p *Process) MemoryMapsWithContext(ctx context.Context, grouped bool) (*[]MemoryMapsStat, error) { pid := p.Pid var ret []MemoryMapsStat - smapsPath := common.HostProc(strconv.Itoa(int(pid)), "smaps") + smapsPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "smaps") if grouped { ret = make([]MemoryMapsStat, 1) // If smaps_rollup exists (require kernel >= 4.15), then we will use it // for pre-summed memory information for a process. - smapsRollupPath := common.HostProc(strconv.Itoa(int(pid)), "smaps_rollup") + smapsRollupPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "smaps_rollup") if _, err := os.Stat(smapsRollupPath); !os.IsNotExist(err) { smapsPath = smapsRollupPath } @@ -481,7 +482,7 @@ func (p *Process) MemoryMapsWithContext(ctx context.Context, grouped bool) (*[]M } func (p *Process) EnvironWithContext(ctx context.Context) ([]string, error) { - environPath := common.HostProc(strconv.Itoa(int(p.Pid)), "environ") + environPath := common.HostProcWithContext(ctx, strconv.Itoa(int(p.Pid)), "environ") environContent, err := ioutil.ReadFile(environPath) if err != nil { @@ -507,9 +508,9 @@ func limitToUint(val string) (uint64, error) { } // Get num_fds from /proc/(pid)/limits -func (p *Process) fillFromLimitsWithContext() ([]RlimitStat, error) { +func (p *Process) fillFromLimitsWithContext(ctx context.Context) ([]RlimitStat, error) { pid := p.Pid - limitsFile := common.HostProc(strconv.Itoa(int(pid)), "limits") + limitsFile := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "limits") d, err := os.Open(limitsFile) if err != nil { return nil, err @@ -602,7 +603,7 @@ func (p *Process) fillFromLimitsWithContext() ([]RlimitStat, error) { // Get list of /proc/(pid)/fd files func (p *Process) fillFromfdListWithContext(ctx context.Context) (string, []string, error) { pid := p.Pid - statPath := common.HostProc(strconv.Itoa(int(pid)), "fd") + statPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "fd") d, err := os.Open(statPath) if err != nil { return statPath, []string{}, err @@ -642,9 +643,9 @@ func (p *Process) fillFromfdWithContext(ctx context.Context) (int32, []*OpenFile } // Get cwd from /proc/(pid)/cwd -func (p *Process) fillFromCwdWithContext() (string, error) { +func (p *Process) fillFromCwdWithContext(ctx context.Context) (string, error) { pid := p.Pid - cwdPath := common.HostProc(strconv.Itoa(int(pid)), "cwd") + cwdPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "cwd") cwd, err := os.Readlink(cwdPath) if err != nil { return "", err @@ -653,9 +654,9 @@ func (p *Process) fillFromCwdWithContext() (string, error) { } // Get exe from /proc/(pid)/exe -func (p *Process) fillFromExeWithContext() (string, error) { +func (p *Process) fillFromExeWithContext(ctx context.Context) (string, error) { pid := p.Pid - exePath := common.HostProc(strconv.Itoa(int(pid)), "exe") + exePath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "exe") exe, err := os.Readlink(exePath) if err != nil { return "", err @@ -666,7 +667,7 @@ func (p *Process) fillFromExeWithContext() (string, error) { // Get cmdline from /proc/(pid)/cmdline func (p *Process) fillFromCmdlineWithContext(ctx context.Context) (string, error) { pid := p.Pid - cmdPath := common.HostProc(strconv.Itoa(int(pid)), "cmdline") + cmdPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "cmdline") cmdline, err := ioutil.ReadFile(cmdPath) if err != nil { return "", err @@ -680,7 +681,7 @@ func (p *Process) fillFromCmdlineWithContext(ctx context.Context) (string, error func (p *Process) fillSliceFromCmdlineWithContext(ctx context.Context) ([]string, error) { pid := p.Pid - cmdPath := common.HostProc(strconv.Itoa(int(pid)), "cmdline") + cmdPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "cmdline") cmdline, err := ioutil.ReadFile(cmdPath) if err != nil { return nil, err @@ -701,9 +702,9 @@ func (p *Process) fillSliceFromCmdlineWithContext(ctx context.Context) ([]string } // Get IO status from /proc/(pid)/io -func (p *Process) fillFromIOWithContext() (*IOCountersStat, error) { +func (p *Process) fillFromIOWithContext(ctx context.Context) (*IOCountersStat, error) { pid := p.Pid - ioPath := common.HostProc(strconv.Itoa(int(pid)), "io") + ioPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "io") ioline, err := ioutil.ReadFile(ioPath) if err != nil { return nil, err @@ -737,9 +738,9 @@ func (p *Process) fillFromIOWithContext() (*IOCountersStat, error) { } // Get memory info from /proc/(pid)/statm -func (p *Process) fillFromStatmWithContext() (*MemoryInfoStat, *MemoryInfoExStat, error) { +func (p *Process) fillFromStatmWithContext(ctx context.Context) (*MemoryInfoStat, *MemoryInfoExStat, error) { pid := p.Pid - memPath := common.HostProc(strconv.Itoa(int(pid)), "statm") + memPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "statm") contents, err := ioutil.ReadFile(memPath) if err != nil { return nil, nil, err @@ -790,7 +791,7 @@ func (p *Process) fillFromStatmWithContext() (*MemoryInfoStat, *MemoryInfoExStat // Get name from /proc/(pid)/comm or /proc/(pid)/status func (p *Process) fillNameWithContext(ctx context.Context) error { - err := p.fillFromCommWithContext() + err := p.fillFromCommWithContext(ctx) if err == nil && p.name != "" && len(p.name) < 15 { return nil } @@ -798,9 +799,9 @@ func (p *Process) fillNameWithContext(ctx context.Context) error { } // Get name from /proc/(pid)/comm -func (p *Process) fillFromCommWithContext() error { +func (p *Process) fillFromCommWithContext(ctx context.Context) error { pid := p.Pid - statPath := common.HostProc(strconv.Itoa(int(pid)), "comm") + statPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "comm") contents, err := ioutil.ReadFile(statPath) if err != nil { return err @@ -817,7 +818,7 @@ func (p *Process) fillFromStatus() error { func (p *Process) fillFromStatusWithContext(ctx context.Context) error { pid := p.Pid - statPath := common.HostProc(strconv.Itoa(int(pid)), "status") + statPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "status") contents, err := ioutil.ReadFile(statPath) if err != nil { return err @@ -844,8 +845,6 @@ func (p *Process) fillFromStatusWithContext(ctx context.Context) error { extendedName := filepath.Base(cmdlineSlice[0]) if strings.HasPrefix(extendedName, p.name) { p.name = extendedName - } else { - p.name = cmdlineSlice[0] } } } @@ -1022,9 +1021,9 @@ func (p *Process) fillFromTIDStatWithContext(ctx context.Context, tid int32) (ui var statPath string if tid == -1 { - statPath = common.HostProc(strconv.Itoa(int(pid)), "stat") + statPath = common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "stat") } else { - statPath = common.HostProc(strconv.Itoa(int(pid)), "task", strconv.Itoa(int(tid)), "stat") + statPath = common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "task", strconv.Itoa(int(tid)), "stat") } contents, err := ioutil.ReadFile(statPath) @@ -1128,7 +1127,7 @@ func (p *Process) fillFromStatWithContext(ctx context.Context) (uint64, int32, * } func pidsWithContext(ctx context.Context) ([]int32, error) { - return readPidsFromDir(common.HostProc()) + return readPidsFromDir(common.HostProcWithContext(ctx)) } func ProcessesWithContext(ctx context.Context) ([]*Process, error) { diff --git a/process/process_linux_test.go b/process/process_linux_test.go index 9003095..e8c2e83 100644 --- a/process/process_linux_test.go +++ b/process/process_linux_test.go @@ -4,6 +4,7 @@ package process import ( + "context" "fmt" "io/ioutil" "os" @@ -11,7 +12,6 @@ import ( "strings" "testing" - "github.com/shirou/gopsutil/v3/internal/common" "github.com/stretchr/testify/assert" ) @@ -62,8 +62,7 @@ func Test_Process_splitProcStat_fromFile(t *testing.T) { if err != nil { t.Error(err) } - f := common.MockEnv("HOST_PROC", "testdata/linux") - defer f() + t.Setenv("HOST_PROC", "testdata/linux") for _, pid := range pids { pid, err := strconv.ParseInt(pid.Name(), 0, 32) if err != nil { @@ -99,8 +98,7 @@ func Test_fillFromCommWithContext(t *testing.T) { if err != nil { t.Error(err) } - f := common.MockEnv("HOST_PROC", "testdata/linux") - defer f() + t.Setenv("HOST_PROC", "testdata/linux") for _, pid := range pids { pid, err := strconv.ParseInt(pid.Name(), 0, 32) if err != nil { @@ -110,7 +108,7 @@ func Test_fillFromCommWithContext(t *testing.T) { continue } p, _ := NewProcess(int32(pid)) - if err := p.fillFromCommWithContext(); err != nil { + if err := p.fillFromCommWithContext(context.Background()); err != nil { t.Error(err) } } @@ -121,8 +119,7 @@ func Test_fillFromStatusWithContext(t *testing.T) { if err != nil { t.Error(err) } - f := common.MockEnv("HOST_PROC", "testdata/linux") - defer f() + t.Setenv("HOST_PROC", "testdata/linux") for _, pid := range pids { pid, err := strconv.ParseInt(pid.Name(), 0, 32) if err != nil { @@ -139,18 +136,16 @@ func Test_fillFromStatusWithContext(t *testing.T) { } func Benchmark_fillFromCommWithContext(b *testing.B) { - f := common.MockEnv("HOST_PROC", "testdata/linux") - defer f() + b.Setenv("HOST_PROC", "testdata/linux") pid := 1060 p, _ := NewProcess(int32(pid)) for i := 0; i < b.N; i++ { - p.fillFromCommWithContext() + p.fillFromCommWithContext(context.Background()) } } func Benchmark_fillFromStatusWithContext(b *testing.B) { - f := common.MockEnv("HOST_PROC", "testdata/linux") - defer f() + b.Setenv("HOST_PROC", "testdata/linux") pid := 1060 p, _ := NewProcess(int32(pid)) for i := 0; i < b.N; i++ { @@ -163,8 +158,7 @@ func Test_fillFromTIDStatWithContext_lx_brandz(t *testing.T) { if err != nil { t.Error(err) } - f := common.MockEnv("HOST_PROC", "testdata/lx_brandz") - defer f() + t.Setenv("HOST_PROC", "testdata/lx_brandz") for _, pid := range pids { pid, err := strconv.ParseInt(pid.Name(), 0, 32) if err != nil { diff --git a/process/process_openbsd.go b/process/process_openbsd.go index cbb1a77..a58c5eb 100644 --- a/process/process_openbsd.go +++ b/process/process_openbsd.go @@ -60,8 +60,6 @@ func (p *Process) NameWithContext(ctx context.Context) (string, error) { extendedName := filepath.Base(cmdlineSlice[0]) if strings.HasPrefix(extendedName, p.name) { name = extendedName - } else { - name = cmdlineSlice[0] } } } diff --git a/process/process_posix.go b/process/process_posix.go index 88e2bff..a01f9ec 100644 --- a/process/process_posix.go +++ b/process/process_posix.go @@ -14,8 +14,9 @@ import ( "strings" "syscall" - "github.com/shirou/gopsutil/v3/internal/common" "golang.org/x/sys/unix" + + "github.com/shirou/gopsutil/v3/internal/common" ) type Signal = syscall.Signal @@ -108,8 +109,8 @@ func PidExistsWithContext(ctx context.Context, pid int32) (bool, error) { return false, err } - if isMount(common.HostProc()) { // if //proc exists and is mounted, check if //proc/ folder exists - _, err := os.Stat(common.HostProc(strconv.Itoa(int(pid)))) + if isMount(common.HostProcWithContext(ctx)) { // if //proc exists and is mounted, check if //proc/ folder exists + _, err := os.Stat(common.HostProcWithContext(ctx, strconv.Itoa(int(pid)))) if os.IsNotExist(err) { return false, nil } @@ -121,7 +122,7 @@ func PidExistsWithContext(ctx context.Context, pid int32) (bool, error) { if err == nil { return true, nil } - if err.Error() == "os: process already finished" { + if errors.Is(err, os.ErrProcessDone) { return false, nil } var errno syscall.Errno diff --git a/process/process_solaris.go b/process/process_solaris.go index 4f10a67..ad1c3cf 100644 --- a/process/process_solaris.go +++ b/process/process_solaris.go @@ -30,7 +30,7 @@ type MemoryMapsStat struct { type MemoryInfoExStat struct{} func pidsWithContext(ctx context.Context) ([]int32, error) { - return readPidsFromDir(common.HostProc()) + return readPidsFromDir(common.HostProcWithContext(ctx)) } func ProcessesWithContext(ctx context.Context) ([]*Process, error) { @@ -199,7 +199,7 @@ func (p *Process) EnvironWithContext(ctx context.Context) ([]string, error) { func (p *Process) fillFromfdListWithContext(ctx context.Context) (string, []string, error) { pid := p.Pid - statPath := common.HostProc(strconv.Itoa(int(pid)), "fd") + statPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "fd") d, err := os.Open(statPath) if err != nil { return statPath, []string{}, err @@ -211,7 +211,7 @@ func (p *Process) fillFromfdListWithContext(ctx context.Context) (string, []stri func (p *Process) fillFromPathCwdWithContext(ctx context.Context) (string, error) { pid := p.Pid - cwdPath := common.HostProc(strconv.Itoa(int(pid)), "path", "cwd") + cwdPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "path", "cwd") cwd, err := os.Readlink(cwdPath) if err != nil { return "", err @@ -221,7 +221,7 @@ func (p *Process) fillFromPathCwdWithContext(ctx context.Context) (string, error func (p *Process) fillFromPathAOutWithContext(ctx context.Context) (string, error) { pid := p.Pid - cwdPath := common.HostProc(strconv.Itoa(int(pid)), "path", "a.out") + cwdPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "path", "a.out") exe, err := os.Readlink(cwdPath) if err != nil { return "", err @@ -231,7 +231,7 @@ func (p *Process) fillFromPathAOutWithContext(ctx context.Context) (string, erro func (p *Process) fillFromExecnameWithContext(ctx context.Context) (string, error) { pid := p.Pid - execNamePath := common.HostProc(strconv.Itoa(int(pid)), "execname") + execNamePath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "execname") exe, err := ioutil.ReadFile(execNamePath) if err != nil { return "", err @@ -241,7 +241,7 @@ func (p *Process) fillFromExecnameWithContext(ctx context.Context) (string, erro func (p *Process) fillFromCmdlineWithContext(ctx context.Context) (string, error) { pid := p.Pid - cmdPath := common.HostProc(strconv.Itoa(int(pid)), "cmdline") + cmdPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "cmdline") cmdline, err := ioutil.ReadFile(cmdPath) if err != nil { return "", err @@ -258,7 +258,7 @@ func (p *Process) fillFromCmdlineWithContext(ctx context.Context) (string, error func (p *Process) fillSliceFromCmdlineWithContext(ctx context.Context) ([]string, error) { pid := p.Pid - cmdPath := common.HostProc(strconv.Itoa(int(pid)), "cmdline") + cmdPath := common.HostProcWithContext(ctx, strconv.Itoa(int(pid)), "cmdline") cmdline, err := ioutil.ReadFile(cmdPath) if err != nil { return nil, err diff --git a/process/process_test.go b/process/process_test.go index 786eb51..fc44963 100644 --- a/process/process_test.go +++ b/process/process_test.go @@ -1,6 +1,7 @@ package process import ( + "bufio" "errors" "fmt" "io/ioutil" @@ -17,8 +18,10 @@ import ( "testing" "time" - "github.com/shirou/gopsutil/v3/internal/common" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/shirou/gopsutil/v3/internal/common" ) var mu sync.Mutex @@ -294,7 +297,7 @@ func Test_Process_Name(t *testing.T) { t.Errorf("getting name error %v", err) } if !strings.Contains(n, "process.test") { - t.Errorf("invalid Exe %s", n) + t.Errorf("invalid Name %s", n) } } @@ -390,6 +393,62 @@ func Test_Process_Long_Name(t *testing.T) { cmd.Process.Kill() } +func Test_Process_Name_Against_Python(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("only applies to posix") + } + py3Path, err := exec.LookPath("python3") + if err != nil { + t.Skipf("python3 not found: %s", err) + } + if out, err := exec.Command(py3Path, "-c", "import psutil").CombinedOutput(); err != nil { + t.Skipf("psutil not found for %s: %s", py3Path, out) + } + + tmpdir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("unable to create temp dir %v", err) + } + defer os.RemoveAll(tmpdir) // clean up + tmpfilepath := filepath.Join(tmpdir, "looooooooooooooooooooong.py") + tmpfile, err := os.Create(tmpfilepath) + if err != nil { + t.Fatalf("unable to create temp file %v", err) + } + tmpfilecontent := []byte("#!" + py3Path + "\nimport psutil, time\nprint(psutil.Process().name(), flush=True)\nwhile True:\n\ttime.sleep(1)") + if _, err := tmpfile.Write(tmpfilecontent); err != nil { + tmpfile.Close() + t.Fatalf("unable to write temp file %v", err) + } + if err := tmpfile.Chmod(0o744); err != nil { + t.Fatalf("unable to chmod u+x temp file %v", err) + } + if err := tmpfile.Close(); err != nil { + t.Fatalf("unable to close temp file %v", err) + } + cmd := exec.Command(tmpfilepath) + outPipe, _ := cmd.StdoutPipe() + scanner := bufio.NewScanner(outPipe) + cmd.Start() + defer cmd.Process.Kill() + scanner.Scan() + pyName := scanner.Text() // first line printed by py3 script, its name + t.Logf("pyName %s", pyName) + p, err := NewProcess(int32(cmd.Process.Pid)) + skipIfNotImplementedErr(t, err) + if err != nil { + t.Fatalf("getting process error %v", err) + } + name, err := p.Name() + skipIfNotImplementedErr(t, err) + if err != nil { + t.Fatalf("getting name error %v", err) + } + if pyName != name { + t.Fatalf("psutil and gopsutil process.Name() results differ: expected %s, got %s", pyName, name) + } +} + func Test_Process_Exe(t *testing.T) { p := testGetProcess() @@ -443,7 +502,7 @@ func Test_Process_CpuPercentLoop(t *testing.T) { } func Test_Process_CreateTime(t *testing.T) { - if os.Getenv("CIRCLECI") == "true" { + if os.Getenv("CI") == "true" { t.Skip("Skip CI") } @@ -784,60 +843,6 @@ func Test_Process_Cwd(t *testing.T) { t.Log(pidCwd) } -func Test_AllProcesses_cmdLine(t *testing.T) { - procs, err := Processes() - skipIfNotImplementedErr(t, err) - if err != nil { - t.Fatalf("getting processes error %v", err) - } - for _, proc := range procs { - var exeName string - var cmdLine string - - exeName, _ = proc.Exe() - cmdLine, err = proc.Cmdline() - if err != nil { - cmdLine = "Error: " + err.Error() - } - - t.Logf("Process #%v: Name: %v / CmdLine: %v\n", proc.Pid, exeName, cmdLine) - } -} - -func Test_AllProcesses_environ(t *testing.T) { - procs, err := Processes() - skipIfNotImplementedErr(t, err) - if err != nil { - t.Fatalf("getting processes error %v", err) - } - for _, proc := range procs { - exeName, _ := proc.Exe() - environ, err := proc.Environ() - if err != nil { - environ = []string{"Error: " + err.Error()} - } - - t.Logf("Process #%v: Name: %v / Environment Variables: %v\n", proc.Pid, exeName, environ) - } -} - -func Test_AllProcesses_Cwd(t *testing.T) { - procs, err := Processes() - skipIfNotImplementedErr(t, err) - if err != nil { - t.Fatalf("getting processes error %v", err) - } - for _, proc := range procs { - exeName, _ := proc.Exe() - cwd, err := proc.Cwd() - if err != nil { - cwd = "Error: " + err.Error() - } - - t.Logf("Process #%v: Name: %v / Current Working Directory: %s\n", proc.Pid, exeName, cwd) - } -} - func BenchmarkNewProcess(b *testing.B) { checkPid := os.Getpid() for i := 0; i < b.N; i++ { @@ -858,3 +863,11 @@ func BenchmarkProcessPpid(b *testing.B) { p.Ppid() } } + +func BenchmarkProcesses(b *testing.B) { + for i := 0; i < b.N; i++ { + ps, err := Processes() + require.NoError(b, err) + require.Greater(b, len(ps), 0) + } +} diff --git a/process/process_windows.go b/process/process_windows.go index 17f7bc8..14ed030 100644 --- a/process/process_windows.go +++ b/process/process_windows.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "os" + "path/filepath" "reflect" "strings" "syscall" @@ -319,18 +320,19 @@ func (p *Process) PpidWithContext(ctx context.Context) (int32, error) { } func (p *Process) NameWithContext(ctx context.Context) (string, error) { - ppid, _, name, err := getFromSnapProcess(p.Pid) - if err != nil { - return "", fmt.Errorf("could not get Name: %s", err) + if p.Pid == 0 { + return "System Idle Process", nil + } + if p.Pid == 4 { + return "System", nil } - // if no errors and not cached already, cache ppid - p.parent = ppid - if 0 == p.getPpid() { - p.setPpid(ppid) + exe, err := p.ExeWithContext(ctx) + if err != nil { + return "", fmt.Errorf("could not get Name: %s", err) } - return name, nil + return filepath.Base(exe), nil } func (p *Process) TgidWithContext(ctx context.Context) (int32, error) { @@ -987,15 +989,9 @@ func is32BitProcess(h windows.Handle) bool { var procIs32Bits bool switch processorArchitecture { - case PROCESSOR_ARCHITECTURE_INTEL: - fallthrough - case PROCESSOR_ARCHITECTURE_ARM: + case PROCESSOR_ARCHITECTURE_INTEL, PROCESSOR_ARCHITECTURE_ARM: procIs32Bits = true - case PROCESSOR_ARCHITECTURE_ARM64: - fallthrough - case PROCESSOR_ARCHITECTURE_IA64: - fallthrough - case PROCESSOR_ARCHITECTURE_AMD64: + case PROCESSOR_ARCHITECTURE_ARM64, PROCESSOR_ARCHITECTURE_IA64, PROCESSOR_ARCHITECTURE_AMD64: var wow64 uint ret, _, _ := common.ProcNtQueryInformationProcess.Call( diff --git a/process/process_windows_32bit.go b/process/process_windows_32bit.go index 982287d..db4d453 100644 --- a/process/process_windows_32bit.go +++ b/process/process_windows_32bit.go @@ -8,9 +8,8 @@ import ( "syscall" "unsafe" - "golang.org/x/sys/windows" - "github.com/shirou/gopsutil/v3/internal/common" + "golang.org/x/sys/windows" ) type PROCESS_MEMORY_COUNTERS struct {