// SPDX-License-Identifier: BSD-3-Clause package process import ( "bufio" "fmt" "io" "net" "os" "os/exec" "os/user" "path/filepath" "reflect" "runtime" "strconv" "strings" "sync" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/shirou/gopsutil/v4/internal/common" ) var mu sync.Mutex func testGetProcess() Process { checkPid := os.Getpid() // process.test ret, _ := NewProcess(int32(checkPid)) return *ret } func TestPids(t *testing.T) { ret, err := Pids() common.SkipIfNotImplementedErr(t, err) require.NoError(t, err) assert.NotEmptyf(t, ret, "could not get pids %v", ret) } func TestPid_exists(t *testing.T) { checkPid := os.Getpid() ret, err := PidExists(int32(checkPid)) common.SkipIfNotImplementedErr(t, err) require.NoError(t, err) assert.Truef(t, ret, "could not get process exists: %v", ret) } func TestNewProcess(t *testing.T) { checkPid := os.Getpid() ret, err := NewProcess(int32(checkPid)) common.SkipIfNotImplementedErr(t, err) require.NoError(t, err) empty := &Process{} if runtime.GOOS != "windows" { // Windows pid is 0 assert.NotSamef(t, empty, ret, "error %v", ret) } } func TestMemoryMaps(t *testing.T) { checkPid := os.Getpid() ret, err := NewProcess(int32(checkPid)) common.SkipIfNotImplementedErr(t, err) require.NoError(t, err) // ungrouped memory maps mmaps, err := ret.MemoryMaps(false) common.SkipIfNotImplementedErr(t, err) require.NoErrorf(t, err, "memory map get error %v", err) empty := MemoryMapsStat{} for _, m := range *mmaps { assert.NotEqualf(t, m, empty, "memory map get error %v", m) } // grouped memory maps mmaps, err = ret.MemoryMaps(true) common.SkipIfNotImplementedErr(t, err) require.NoErrorf(t, err, "memory map get error %v", err) assert.Lenf(t, *mmaps, 1, "grouped memory maps length (%v) is not equal to 1", len(*mmaps)) assert.NotEqualf(t, (*mmaps)[0], empty, "memory map is empty") } func TestMemoryInfo(t *testing.T) { p := testGetProcess() v, err := p.MemoryInfo() common.SkipIfNotImplementedErr(t, err) require.NoErrorf(t, err, "getting memory info error %v", err) empty := MemoryInfoStat{} if v == nil || *v == empty { t.Errorf("could not get memory info %v", v) } } func TestCmdLine(t *testing.T) { p := testGetProcess() v, err := p.Cmdline() common.SkipIfNotImplementedErr(t, err) require.NoErrorf(t, err, "getting cmdline error %v", err) assert.Containsf(t, v, "process.test", "invalid cmd line %v", v) } func TestCmdLineSlice(t *testing.T) { p := testGetProcess() v, err := p.CmdlineSlice() common.SkipIfNotImplementedErr(t, err) require.NoErrorf(t, err, "getting cmdline slice error %v", err) assert.Truef(t, reflect.DeepEqual(v, os.Args), "returned cmdline slice not as expected:\nexp: %v\ngot: %v", os.Args, v) } func TestPpid(t *testing.T) { p := testGetProcess() v, err := p.Ppid() common.SkipIfNotImplementedErr(t, err) require.NoErrorf(t, err, "getting ppid error %v", err) assert.NotZerof(t, v, "return value is 0 %v", v) expected := os.Getppid() assert.Equalf(t, int32(expected), v, "return value is %v, expected %v", v, expected) } func TestStatus(t *testing.T) { p := testGetProcess() v, err := p.Status() common.SkipIfNotImplementedErr(t, err) require.NoErrorf(t, err, "getting status error %v", err) assert.NotEmptyf(t, v, "could not get state") if v[0] != Running && v[0] != Sleep { t.Errorf("got wrong state, %v", v) } } func TestTerminal(t *testing.T) { p := testGetProcess() _, err := p.Terminal() common.SkipIfNotImplementedErr(t, err) assert.NoErrorf(t, err, "getting terminal error %v", err) } func TestIOCounters(t *testing.T) { p := testGetProcess() v, err := p.IOCounters() common.SkipIfNotImplementedErr(t, err) require.NoErrorf(t, err, "getting iocounter error %v", err) empty := &IOCountersStat{} assert.NotSamef(t, v, empty, "error %v", v) } func TestNumCtx(t *testing.T) { p := testGetProcess() _, err := p.NumCtxSwitches() common.SkipIfNotImplementedErr(t, err) assert.NoErrorf(t, err, "getting numctx error %v", err) } func TestNice(t *testing.T) { p := testGetProcess() // https://github.com/shirou/gopsutil/issues/1532 if os.Getenv("CI") == "true" && runtime.GOOS == "darwin" { t.Skip("Skip CI") } n, err := p.Nice() common.SkipIfNotImplementedErr(t, err) require.NoErrorf(t, err, "getting nice error %v", err) if runtime.GOOS != "windows" && n != 0 && n != 20 && n != 8 { t.Errorf("invalid nice: %d", n) } } func TestGroups(t *testing.T) { p := testGetProcess() v, err := p.Groups() common.SkipIfNotImplementedErr(t, err) require.NoErrorf(t, err, "getting groups error %v", err) if len(v) == 0 { t.Skip("Groups is empty") } } func TestNumThread(t *testing.T) { p := testGetProcess() n, err := p.NumThreads() common.SkipIfNotImplementedErr(t, err) require.NoErrorf(t, err, "getting NumThread error %v", err) assert.GreaterOrEqualf(t, n, int32(0), "invalid NumThread: %d", n) } func TestThreads(t *testing.T) { p := testGetProcess() n, err := p.NumThreads() common.SkipIfNotImplementedErr(t, err) require.NoErrorf(t, err, "getting NumThread error %v", err) assert.GreaterOrEqualf(t, n, int32(0), "invalid NumThread: %d", n) ts, err := p.Threads() common.SkipIfNotImplementedErr(t, err) require.NoErrorf(t, err, "getting Threads error %v", err) assert.Equalf(t, len(ts), int(n), "unexpected number of threads: %v vs %v", len(ts), n) } func TestName(t *testing.T) { p := testGetProcess() n, err := p.Name() common.SkipIfNotImplementedErr(t, err) require.NoErrorf(t, err, "getting name error %v", err) assert.Containsf(t, n, "process.test", "invalid Name %s", n) } // #nosec G204 func TestLong_Name_With_Spaces(t *testing.T) { tmpdir, err := os.MkdirTemp("", "") require.NoErrorf(t, err, "unable to create temp dir %v", err) defer os.RemoveAll(tmpdir) // clean up tmpfilepath := filepath.Join(tmpdir, "loooong name with spaces.go") tmpfile, err := os.Create(tmpfilepath) require.NoErrorf(t, err, "unable to create temp file %v", err) tmpfilecontent := []byte("package main\nimport(\n\"time\"\n)\nfunc main(){\nfor range time.Tick(time.Second) {}\n}") if _, err := tmpfile.Write(tmpfilecontent); err != nil { tmpfile.Close() t.Fatalf("unable to write temp file %v", err) } require.NoErrorf(t, tmpfile.Close(), "unable to close temp file") err = exec.Command("go", "build", "-o", tmpfile.Name()+".exe", tmpfile.Name()).Run() require.NoErrorf(t, err, "unable to build temp file %v", err) cmd := exec.Command(tmpfile.Name() + ".exe") require.NoError(t, cmd.Start()) time.Sleep(100 * time.Millisecond) p, err := NewProcess(int32(cmd.Process.Pid)) common.SkipIfNotImplementedErr(t, err) require.NoError(t, err) n, err := p.Name() common.SkipIfNotImplementedErr(t, err) require.NoErrorf(t, err, "getting name error %v", err) basename := filepath.Base(tmpfile.Name() + ".exe") require.Equalf(t, basename, n, "%s != %s", basename, n) cmd.Process.Kill() } // #nosec G204 func TestLong_Name(t *testing.T) { tmpdir, err := os.MkdirTemp("", "") require.NoErrorf(t, err, "unable to create temp dir %v", err) defer os.RemoveAll(tmpdir) // clean up tmpfilepath := filepath.Join(tmpdir, "looooooooooooooooooooong.go") tmpfile, err := os.Create(tmpfilepath) require.NoErrorf(t, err, "unable to create temp file %v", err) tmpfilecontent := []byte("package main\nimport(\n\"time\"\n)\nfunc main(){\nfor range time.Tick(time.Second) {}\n}") if _, err := tmpfile.Write(tmpfilecontent); err != nil { tmpfile.Close() t.Fatalf("unable to write temp file %v", err) } require.NoErrorf(t, tmpfile.Close(), "unable to close temp file") err = exec.Command("go", "build", "-o", tmpfile.Name()+".exe", tmpfile.Name()).Run() require.NoErrorf(t, err, "unable to build temp file %v", err) cmd := exec.Command(tmpfile.Name() + ".exe") require.NoError(t, cmd.Start()) time.Sleep(100 * time.Millisecond) p, err := NewProcess(int32(cmd.Process.Pid)) common.SkipIfNotImplementedErr(t, err) require.NoError(t, err) n, err := p.Name() common.SkipIfNotImplementedErr(t, err) require.NoErrorf(t, err, "getting name error %v", err) basename := filepath.Base(tmpfile.Name() + ".exe") require.Equalf(t, basename, n, "%s != %s", basename, n) cmd.Process.Kill() } func TestName_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 := os.MkdirTemp("", "") require.NoErrorf(t, err, "unable to create temp dir %v", err) defer os.RemoveAll(tmpdir) // clean up tmpfilepath := filepath.Join(tmpdir, "looooooooooooooooooooong.py") tmpfile, err := os.Create(tmpfilepath) require.NoErrorf(t, err, "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) } require.NoErrorf(t, tmpfile.Chmod(0o744), "unable to chmod u+x temp file") require.NoErrorf(t, tmpfile.Close(), "unable to close temp file") 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)) common.SkipIfNotImplementedErr(t, err) require.NoErrorf(t, err, "getting process error %v", err) name, err := p.Name() common.SkipIfNotImplementedErr(t, err) require.NoErrorf(t, err, "getting name error %v", err) require.Equalf(t, pyName, name, "psutil and gopsutil process.Name() results differ: expected %s, got %s", pyName, name) } func TestExe(t *testing.T) { p := testGetProcess() n, err := p.Exe() common.SkipIfNotImplementedErr(t, err) require.NoErrorf(t, err, "getting Exe error %v", err) assert.Containsf(t, n, "process.test", "invalid Exe %s", n) } func TestCpuPercent(t *testing.T) { p := testGetProcess() _, err := p.Percent(0) common.SkipIfNotImplementedErr(t, err) require.NoError(t, err) duration := time.Duration(1000) * time.Microsecond time.Sleep(duration) percent, err := p.Percent(0) require.NoError(t, err) numcpu := runtime.NumCPU() // if percent < 0.0 || percent > 100.0*float64(numcpu) { // TODO require.GreaterOrEqualf(t, percent, 0.0, "CPUPercent value is invalid: %f, %d", percent, numcpu) } func TestCpuPercentLoop(t *testing.T) { p := testGetProcess() numcpu := runtime.NumCPU() for i := 0; i < 2; i++ { duration := time.Duration(100) * time.Microsecond percent, err := p.Percent(duration) common.SkipIfNotImplementedErr(t, err) require.NoError(t, err) // if percent < 0.0 || percent > 100.0*float64(numcpu) { // TODO require.GreaterOrEqualf(t, percent, 0.0, "CPUPercent value is invalid: %f, %d", percent, numcpu) } } func TestCreateTime(t *testing.T) { if os.Getenv("CI") == "true" { t.Skip("Skip CI") } p := testGetProcess() c, err := p.CreateTime() common.SkipIfNotImplementedErr(t, err) require.NoError(t, err) assert.GreaterOrEqualf(t, c, 1420000000, "process created time is wrong.") gotElapsed := time.Since(time.Unix(int64(c/1000), 0)) maxElapsed := time.Duration(20 * time.Second) assert.Lessf(t, gotElapsed, maxElapsed, "this process has not been running for %v", gotElapsed) } func TestParent(t *testing.T) { p := testGetProcess() c, err := p.Parent() common.SkipIfNotImplementedErr(t, err) require.NoError(t, err) require.NotNilf(t, c, "could not get parent") require.NotZerof(t, c.Pid, "wrong parent pid") } func TestConnections(t *testing.T) { p := testGetProcess() addr, err := net.ResolveTCPAddr("tcp", "localhost:0") // dynamically get a random open port from OS require.NoErrorf(t, err, "unable to resolve localhost: %v", err) l, err := net.ListenTCP(addr.Network(), addr) require.NoErrorf(t, err, "unable to listen on %v: %v", addr, err) defer l.Close() tcpServerAddr := l.Addr().String() tcpServerAddrIP := strings.Split(tcpServerAddr, ":")[0] tcpServerAddrPort, err := strconv.ParseUint(strings.Split(tcpServerAddr, ":")[1], 10, 32) require.NoErrorf(t, err, "unable to parse tcpServerAddr port: %v", err) serverEstablished := make(chan struct{}) go func() { // TCP listening goroutine conn, err := l.Accept() if err != nil { panic(err) } defer conn.Close() serverEstablished <- struct{}{} _, err = io.ReadAll(conn) if err != nil { panic(err) } }() conn, err := net.Dial("tcp", tcpServerAddr) require.NoErrorf(t, err, "unable to dial %v: %v", tcpServerAddr, err) defer conn.Close() // Rarely the call to net.Dial returns before the server connection is // established. Wait so that the test doesn't fail. <-serverEstablished c, err := p.Connections() common.SkipIfNotImplementedErr(t, err) require.NoError(t, err) require.NotEmptyf(t, c, "no connections found") serverConnections := 0 for _, connection := range c { if connection.Laddr.IP == tcpServerAddrIP && connection.Laddr.Port == uint32(tcpServerAddrPort) && connection.Raddr.Port != 0 { require.Equalf(t, "ESTABLISHED", connection.Status, "expected server connection to be ESTABLISHED, have %+v", connection) serverConnections++ } } clientConnections := 0 for _, connection := range c { if connection.Raddr.IP == tcpServerAddrIP && connection.Raddr.Port == uint32(tcpServerAddrPort) { require.Equalf(t, "ESTABLISHED", connection.Status, "expected client connection to be ESTABLISHED, have %+v", connection) clientConnections++ } } // two established connections, one for the server, the other for the client require.Equalf(t, 1, serverConnections, "expected 1 server connection, have %d.\nDetails: %+v", serverConnections, c) // two established connections, one for the server, the other for the client require.Equalf(t, 1, clientConnections, "expected 1 server connection, have %d.\nDetails: %+v", clientConnections, c) } func TestChildren(t *testing.T) { p := testGetProcess() var cmd *exec.Cmd if runtime.GOOS == "windows" { cmd = exec.Command("ping", "localhost", "-n", "4") } else { cmd = exec.Command("sleep", "3") } require.NoError(t, cmd.Start()) time.Sleep(100 * time.Millisecond) c, err := p.Children() common.SkipIfNotImplementedErr(t, err) require.NoError(t, err) require.NotEmptyf(t, c, "children is empty") found := false for _, child := range c { if child.Pid == int32(cmd.Process.Pid) { found = true break } } assert.Truef(t, found, "could not find child %d", cmd.Process.Pid) } func TestUsername(t *testing.T) { myPid := os.Getpid() currentUser, _ := user.Current() myUsername := currentUser.Username process, _ := NewProcess(int32(myPid)) pidUsername, err := process.Username() common.SkipIfNotImplementedErr(t, err) assert.Equal(t, myUsername, pidUsername) t.Log(pidUsername) } func TestCPUTimes(t *testing.T) { pid := os.Getpid() process, err := NewProcess(int32(pid)) common.SkipIfNotImplementedErr(t, err) require.NoError(t, err) spinSeconds := 0.2 cpuTimes0, err := process.Times() common.SkipIfNotImplementedErr(t, err) require.NoError(t, err) // Spin for a duration of spinSeconds t0 := time.Now() tGoal := t0.Add(time.Duration(spinSeconds*1000) * time.Millisecond) require.NoError(t, err) for time.Now().Before(tGoal) { // This block intentionally left blank } cpuTimes1, err := process.Times() require.NoError(t, err) if cpuTimes0 == nil || cpuTimes1 == nil { t.FailNow() } measuredElapsed := cpuTimes1.Total() - cpuTimes0.Total() message := fmt.Sprintf("Measured %fs != spun time of %fs\ncpuTimes0=%v\ncpuTimes1=%v", measuredElapsed, spinSeconds, cpuTimes0, cpuTimes1) assert.Greaterf(t, measuredElapsed, float64(spinSeconds)/5, message) assert.Lessf(t, measuredElapsed, float64(spinSeconds)*5, message) } func TestOpenFiles(t *testing.T) { fp, err := os.Open("process_test.go") require.NoError(t, err) defer func() { assert.NoError(t, fp.Close()) }() pid := os.Getpid() p, err := NewProcess(int32(pid)) common.SkipIfNotImplementedErr(t, err) require.NoError(t, err) v, err := p.OpenFiles() common.SkipIfNotImplementedErr(t, err) require.NoError(t, err) assert.NotEmpty(t, v) // test always open files. for _, vv := range v { assert.NotEqual(t, "", vv.Path) } } func TestKill(t *testing.T) { var cmd *exec.Cmd if runtime.GOOS == "windows" { cmd = exec.Command("ping", "localhost", "-n", "4") } else { cmd = exec.Command("sleep", "3") } require.NoError(t, cmd.Start()) time.Sleep(100 * time.Millisecond) p, err := NewProcess(int32(cmd.Process.Pid)) common.SkipIfNotImplementedErr(t, err) require.NoError(t, err) err = p.Kill() common.SkipIfNotImplementedErr(t, err) require.NoError(t, err) cmd.Wait() } func TestIsRunning(t *testing.T) { var cmd *exec.Cmd if runtime.GOOS == "windows" { cmd = exec.Command("ping", "localhost", "-n", "2") } else { cmd = exec.Command("sleep", "1") } cmd.Start() p, err := NewProcess(int32(cmd.Process.Pid)) common.SkipIfNotImplementedErr(t, err) require.NoError(t, err) running, err := p.IsRunning() common.SkipIfNotImplementedErr(t, err) require.NoErrorf(t, err, "IsRunning error: %v", err) require.Truef(t, running, "process should be found running") cmd.Wait() running, err = p.IsRunning() common.SkipIfNotImplementedErr(t, err) require.NoErrorf(t, err, "IsRunning error: %v", err) require.Falsef(t, running, "process should NOT be found running") } // #nosec G204 func TestEnviron(t *testing.T) { tmpdir, err := os.MkdirTemp("", "") require.NoErrorf(t, err, "unable to create temp dir %v", err) defer os.RemoveAll(tmpdir) // clean up tmpfilepath := filepath.Join(tmpdir, "test.go") tmpfile, err := os.Create(tmpfilepath) require.NoErrorf(t, err, "unable to create temp file %v", err) tmpfilecontent := []byte("package main\nimport(\n\"time\"\n)\nfunc main(){\nfor range time.Tick(time.Second) {}\n}") if _, err := tmpfile.Write(tmpfilecontent); err != nil { tmpfile.Close() t.Fatalf("unable to write temp file %v", err) } require.NoErrorf(t, tmpfile.Close(), "unable to close temp file") err = exec.Command("go", "build", "-o", tmpfile.Name()+".exe", tmpfile.Name()).Run() require.NoErrorf(t, err, "unable to build temp file %v", err) cmd := exec.Command(tmpfile.Name() + ".exe") cmd.Env = []string{"testkey=envvalue"} require.NoError(t, cmd.Start()) defer cmd.Process.Kill() time.Sleep(100 * time.Millisecond) p, err := NewProcess(int32(cmd.Process.Pid)) common.SkipIfNotImplementedErr(t, err) require.NoError(t, err) envs, err := p.Environ() common.SkipIfNotImplementedErr(t, err) require.NoErrorf(t, err, "getting environ error %v", err) var envvarFound bool for _, envvar := range envs { if envvar == "testkey=envvalue" { envvarFound = true break } } assert.Truef(t, envvarFound, "environment variable not found") } func TestCwd(t *testing.T) { myPid := os.Getpid() currentWorkingDirectory, _ := os.Getwd() process, _ := NewProcess(int32(myPid)) pidCwd, err := process.Cwd() common.SkipIfNotImplementedErr(t, err) require.NoErrorf(t, err, "getting cwd error %v", err) pidCwd = strings.TrimSuffix(pidCwd, string(os.PathSeparator)) assert.Equal(t, currentWorkingDirectory, pidCwd) t.Log(pidCwd) } func BenchmarkNewProcess(b *testing.B) { checkPid := os.Getpid() for i := 0; i < b.N; i++ { NewProcess(int32(checkPid)) } } func BenchmarkProcessName(b *testing.B) { p := testGetProcess() for i := 0; i < b.N; i++ { p.Name() } } func BenchmarkProcessPpid(b *testing.B) { p := testGetProcess() for i := 0; i < b.N; i++ { p.Ppid() } } func BenchmarkProcesses(b *testing.B) { for i := 0; i < b.N; i++ { ps, err := Processes() require.NoError(b, err) require.NotEmpty(b, ps) } }