os: add (*Process).WithHandle

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>
This commit is contained in:
Kir Kolyshkin 2025-08-27 22:39:25 -07:00 committed by Gopher Robot
parent 3573227fe3
commit 8ace10dad2
8 changed files with 190 additions and 0 deletions

2
api/next/70352.txt Normal file
View file

@ -0,0 +1,2 @@
pkg os, method (*Process) WithHandle(func(uintptr)) error #70352
pkg os, var ErrNoHandle error #70352

View file

@ -0,0 +1,4 @@
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.

View file

@ -19,6 +19,8 @@ var (
ErrProcessDone = errors.New("os: process already finished") ErrProcessDone = errors.New("os: process already finished")
// errProcessReleased indicates a [Process] has been released. // errProcessReleased indicates a [Process] has been released.
errProcessReleased = errors.New("os: process already 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]. // processStatus describes the status of a [Process].
@ -350,6 +352,18 @@ func (p *Process) Signal(sig Signal) error {
return p.signal(sig) 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. // UserTime returns the user CPU time of the exited process and its children.
func (p *ProcessState) UserTime() time.Duration { func (p *ProcessState) UserTime() time.Duration {
return p.userTime() return p.userTime()

View file

@ -0,0 +1,40 @@
// 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)
}
}

View file

@ -88,6 +88,10 @@ func (p *Process) wait() (ps *ProcessState, err error) {
return ps, nil return ps, nil
} }
func (p *Process) withHandle(_ func(handle uintptr)) error {
return ErrNoHandle
}
func findProcess(pid int) (p *Process, err error) { func findProcess(pid int) (p *Process, err error) {
// NOOP for Plan 9. // NOOP for Plan 9.
return newPIDProcess(pid), nil return newPIDProcess(pid), nil

View file

@ -77,6 +77,23 @@ func (p *Process) kill() error {
return p.Signal(Kill) 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. // ProcessState stores information about a process, as reported by Wait.
type ProcessState struct { type ProcessState struct {
pid int // The process's id. pid int // The process's id.

View file

@ -12,7 +12,9 @@ import (
. "os" . "os"
"path/filepath" "path/filepath"
"sync" "sync"
"syscall"
"testing" "testing"
"time"
) )
func TestRemoveAllWithExecutedProcess(t *testing.T) { func TestRemoveAllWithExecutedProcess(t *testing.T) {
@ -81,3 +83,39 @@ func TestRemoveAllWithExecutedProcess(t *testing.T) {
} }
wg.Wait() 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")
}
}

View file

@ -12,6 +12,7 @@ import (
"os/exec" "os/exec"
"syscall" "syscall"
"testing" "testing"
"time"
) )
func TestFindProcessViaPidfd(t *testing.T) { func TestFindProcessViaPidfd(t *testing.T) {
@ -145,3 +146,73 @@ func TestPidfdLeak(t *testing.T) {
t.Errorf("got descriptor %d, want %d", got[count-1], want[count-1]) 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")
}
}
}