mirror of
https://github.com/golang/go.git
synced 2025-12-08 06:10:04 +00:00
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:
parent
3573227fe3
commit
8ace10dad2
8 changed files with 190 additions and 0 deletions
2
api/next/70352.txt
Normal file
2
api/next/70352.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pkg os, method (*Process) WithHandle(func(uintptr)) error #70352
|
||||
pkg os, var ErrNoHandle error #70352
|
||||
4
doc/next/6-stdlib/99-minor/os/70352.md
Normal file
4
doc/next/6-stdlib/99-minor/os/70352.md
Normal 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.
|
||||
|
|
@ -19,6 +19,8 @@ var (
|
|||
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].
|
||||
|
|
@ -350,6 +352,18 @@ func (p *Process) Signal(sig Signal) error {
|
|||
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()
|
||||
|
|
|
|||
40
src/os/exec_nohandle_test.go
Normal file
40
src/os/exec_nohandle_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -88,6 +88,10 @@ func (p *Process) wait() (ps *ProcessState, err error) {
|
|||
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
|
||||
|
|
|
|||
|
|
@ -77,6 +77,23 @@ func (p *Process) kill() error {
|
|||
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.
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ import (
|
|||
. "os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRemoveAllWithExecutedProcess(t *testing.T) {
|
||||
|
|
@ -81,3 +83,39 @@ 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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"os/exec"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
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])
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue