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")
|
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()
|
||||||
|
|
|
||||||
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
|
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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue