Implement (*Process).WithHandle, add tests for all platforms.
Fixes #70352
Change-Id: I7a8012fb4e1e1b4ce1e75a59403ff6e77504fc56
Reviewed-on: https://go-review.googlesource.com/c/go/+/699615
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Mark Freeman <markfreeman@google.com>
Auto-Submit: Kirill Kolyshkin <kolyshkin@gmail.com>
Reviewed-by: Michael Pratt <mpratt@google.com>
--- /dev/null
+pkg os, method (*Process) WithHandle(func(uintptr)) error #70352
+pkg os, var ErrNoHandle error #70352
--- /dev/null
+The new [Process.WithHandle] method provides access to an internal process
+handle on supported platforms (Linux 5.4 or later and Windows). On Linux,
+the process handle is a pidfd. The method returns [ErrNoHandle] on unsupported
+platforms or when no process handle is available.
ErrProcessDone = errors.New("os: process already finished")
// errProcessReleased indicates a [Process] has been released.
errProcessReleased = errors.New("os: process already released")
+ // ErrNoHandle indicates a [Process] does not have a handle.
+ ErrNoHandle = errors.New("os: process handle unavailable")
)
// processStatus describes the status of a [Process].
return p.signal(sig)
}
+// WithHandle calls a supplied function f with a valid process handle
+// as an argument. The handle is guaranteed to refer to process p
+// until f returns, even if p terminates. This function cannot be used
+// after [Process.Release] or [Process.Wait].
+//
+// If process handles are not supported or a handle is not available,
+// it returns [ErrNoHandle]. Currently, process handles are supported
+// on Linux 5.4 or later (pidfd) and Windows.
+func (p *Process) WithHandle(f func(handle uintptr)) error {
+ return p.withHandle(f)
+}
+
// UserTime returns the user CPU time of the exited process and its children.
func (p *ProcessState) UserTime() time.Duration {
return p.userTime()
--- /dev/null
+// Copyright 2025 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build !linux && !windows
+
+package os_test
+
+import (
+ "internal/testenv"
+ . "os"
+ "testing"
+ "time"
+)
+
+func TestProcessWithHandleUnsupported(t *testing.T) {
+ const envVar = "OSTEST_PROCESS_WITH_HANDLE"
+ if Getenv(envVar) != "" {
+ time.Sleep(1 * time.Minute)
+ return
+ }
+
+ cmd := testenv.CommandContext(t, t.Context(), testenv.Executable(t), "-test.run=^"+t.Name()+"$")
+ cmd = testenv.CleanCmdEnv(cmd)
+ cmd.Env = append(cmd.Env, envVar+"=1")
+ if err := cmd.Start(); err != nil {
+ t.Fatal(err)
+ }
+ defer func() {
+ cmd.Process.Kill()
+ cmd.Wait()
+ }()
+
+ err := cmd.Process.WithHandle(func(handle uintptr) {
+ t.Errorf("WithHandle: callback called unexpectedly with handle=%v", handle)
+ })
+ if err != ErrNoHandle {
+ t.Fatalf("WithHandle: got error %v, want %v", err, ErrNoHandle)
+ }
+}
return ps, nil
}
+func (p *Process) withHandle(_ func(handle uintptr)) error {
+ return ErrNoHandle
+}
+
func findProcess(pid int) (p *Process, err error) {
// NOOP for Plan 9.
return newPIDProcess(pid), nil
return p.Signal(Kill)
}
+func (p *Process) withHandle(f func(handle uintptr)) error {
+ if p.handle == nil {
+ return ErrNoHandle
+ }
+ handle, status := p.handleTransientAcquire()
+ switch status {
+ case statusDone:
+ return ErrProcessDone
+ case statusReleased:
+ return errProcessReleased
+ }
+ defer p.handleTransientRelease()
+ f(handle)
+
+ return nil
+}
+
// ProcessState stores information about a process, as reported by Wait.
type ProcessState struct {
pid int // The process's id.
. "os"
"path/filepath"
"sync"
+ "syscall"
"testing"
+ "time"
)
func TestRemoveAllWithExecutedProcess(t *testing.T) {
}
wg.Wait()
}
+
+func TestProcessWithHandleWindows(t *testing.T) {
+ const envVar = "OSTEST_PROCESS_WITH_HANDLE"
+ if Getenv(envVar) != "" {
+ time.Sleep(1 * time.Minute)
+ return
+ }
+
+ cmd := testenv.CommandContext(t, t.Context(), testenv.Executable(t), "-test.run=^"+t.Name()+"$")
+ cmd = testenv.CleanCmdEnv(cmd)
+ cmd.Env = append(cmd.Env, envVar+"=1")
+ if err := cmd.Start(); err != nil {
+ t.Fatal(err)
+ }
+ defer func() {
+ cmd.Process.Kill()
+ cmd.Wait()
+ }()
+
+ called := false
+ err := cmd.Process.WithHandle(func(handle uintptr) {
+ called = true
+ // Check the handle is valid.
+ var u syscall.Rusage
+ e := syscall.GetProcessTimes(syscall.Handle(handle), &u.CreationTime, &u.ExitTime, &u.KernelTime, &u.UserTime)
+ if e != nil {
+ t.Errorf("Using process handle failed: %v", NewSyscallError("GetProcessTimes", e))
+ }
+ })
+ if err != nil {
+ t.Fatalf("WithHandle: got error %v, want nil", err)
+ }
+ if !called {
+ t.Fatal("WithHandle did not call the callback function")
+ }
+}
"os/exec"
"syscall"
"testing"
+ "time"
)
func TestFindProcessViaPidfd(t *testing.T) {
t.Errorf("got descriptor %d, want %d", got[count-1], want[count-1])
}
}
+
+func TestProcessWithHandleLinux(t *testing.T) {
+ t.Parallel()
+ havePidfd := os.CheckPidfdOnce() == nil
+
+ const envVar = "OSTEST_PROCESS_WITH_HANDLE"
+ if os.Getenv(envVar) != "" {
+ time.Sleep(1 * time.Minute)
+ return
+ }
+
+ cmd := testenv.CommandContext(t, t.Context(), testenv.Executable(t), "-test.run=^"+t.Name()+"$")
+ cmd = testenv.CleanCmdEnv(cmd)
+ cmd.Env = append(cmd.Env, envVar+"=1")
+ if err := cmd.Start(); err != nil {
+ t.Fatal(err)
+ }
+ defer func() {
+ cmd.Process.Kill()
+ cmd.Wait()
+ }()
+
+ const sig = syscall.SIGINT
+ called := false
+ err := cmd.Process.WithHandle(func(pidfd uintptr) {
+ called = true
+ // Check the provided pidfd is valid, and terminate the child.
+ err := unix.PidFDSendSignal(pidfd, sig)
+ if err != nil {
+ t.Errorf("PidFDSendSignal: got error %v, want nil", err)
+ }
+ })
+ // If pidfd is not supported, WithHandle should fail.
+ if !havePidfd && err == nil {
+ t.Fatal("WithHandle: got nil, want error")
+ }
+ // If pidfd is supported, WithHandle should succeed.
+ if havePidfd && err != nil {
+ t.Fatalf("WithHandle: got error %v, want nil", err)
+ }
+ // If pidfd is supported, function should have been called, and vice versa.
+ if havePidfd != called {
+ t.Fatalf("WithHandle: havePidfd is %v, but called is %v", havePidfd, called)
+ }
+ // If pidfd is supported, wait on the child process to check it worked as intended.
+ if called {
+ err := cmd.Wait()
+ if err == nil {
+ t.Fatal("Wait: want error, got nil")
+ }
+ st := cmd.ProcessState.Sys().(syscall.WaitStatus)
+ if !st.Signaled() {
+ t.Fatal("ProcessState: want Signaled, got", err)
+ }
+ if gotSig := st.Signal(); sig != gotSig {
+ t.Fatalf("ProcessState.Signal: want %v, got %v", sig, gotSig)
+ }
+ // Finally, check that WithHandle now returns ErrProcessDone.
+ called = false
+ err = cmd.Process.WithHandle(func(_ uintptr) {
+ called = true
+ })
+ if err != os.ErrProcessDone {
+ t.Fatalf("WithHandle: want os.ErrProcessDone, got %v", err)
+ }
+ if called {
+ t.Fatal("called: want false, got true")
+ }
+ }
+}