]> Cypherpunks repositories - gostls13.git/commitdiff
os: add (*Process).WithHandle
authorKir Kolyshkin <kolyshkin@gmail.com>
Thu, 28 Aug 2025 05:39:25 +0000 (22:39 -0700)
committerGopher Robot <gobot@golang.org>
Mon, 15 Sep 2025 19:17:42 +0000 (12:17 -0700)
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>
api/next/70352.txt [new file with mode: 0644]
doc/next/6-stdlib/99-minor/os/70352.md [new file with mode: 0644]
src/os/exec.go
src/os/exec_nohandle_test.go [new file with mode: 0644]
src/os/exec_plan9.go
src/os/exec_posix.go
src/os/exec_windows_test.go
src/os/pidfd_linux_test.go

diff --git a/api/next/70352.txt b/api/next/70352.txt
new file mode 100644 (file)
index 0000000..cd26404
--- /dev/null
@@ -0,0 +1,2 @@
+pkg os, method (*Process) WithHandle(func(uintptr)) error #70352
+pkg os, var ErrNoHandle error #70352
diff --git a/doc/next/6-stdlib/99-minor/os/70352.md b/doc/next/6-stdlib/99-minor/os/70352.md
new file mode 100644 (file)
index 0000000..5651639
--- /dev/null
@@ -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.
index e14434d8b5d36f994d4dea0c5af9529d52f5169b..8b164ad6673bb9d436bf0096e870402b70e175d3 100644 (file)
@@ -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()
diff --git a/src/os/exec_nohandle_test.go b/src/os/exec_nohandle_test.go
new file mode 100644 (file)
index 0000000..73a8ada
--- /dev/null
@@ -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)
+       }
+}
index 357b925b36081331089d0ea1d10e5aaad2869bc2..a3d363b344061fbdccdb588e4f7444022ef51de7 100644 (file)
@@ -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
index ff51247d56b72d8582df9e4acaf3a672df3f3e95..6b6977ab7855911b12eead430435932ae0428e1f 100644 (file)
@@ -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.
index f8ed4cdf1c9266efbf6c4d18b4c13ca0e6803a9e..272395ca9298372013be84428f638a7a74b2e2e8 100644 (file)
@@ -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")
+       }
+}
index 6b10798dd47c6b6e9606fe81a3d7457d59f2e44b..0210850c0450e6b3ca669a4571632806319b79ee 100644 (file)
@@ -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")
+               }
+       }
+}