feat(process): add signal task surface
Co-authored-by: Virgil <virgil@lethean.io>
This commit is contained in:
parent
04543700bc
commit
79e2ffa6ed
5 changed files with 251 additions and 1 deletions
19
actions.go
19
actions.go
|
|
@ -1,6 +1,9 @@
|
|||
package process
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// --- ACTION messages (broadcast via Core.ACTION) ---
|
||||
|
||||
|
|
@ -62,6 +65,20 @@ type TaskProcessKill struct {
|
|||
PID int
|
||||
}
|
||||
|
||||
// TaskProcessSignal requests signalling a managed process by ID or PID through Core.PERFORM.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// c.PERFORM(process.TaskProcessSignal{ID: "proc-1", Signal: syscall.SIGTERM})
|
||||
type TaskProcessSignal struct {
|
||||
// ID identifies a managed process started by this service.
|
||||
ID string
|
||||
// PID targets a process directly when ID is not available.
|
||||
PID int
|
||||
// Signal is delivered to the process or process group.
|
||||
Signal syscall.Signal
|
||||
}
|
||||
|
||||
// TaskProcessGet requests a snapshot of a managed process through Core.PERFORM.
|
||||
//
|
||||
// Example:
|
||||
|
|
|
|||
|
|
@ -2,8 +2,11 @@ package process
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
framework "dappco.re/go/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -270,6 +273,68 @@ func TestGlobal_Output(t *testing.T) {
|
|||
assert.Contains(t, output, "global-output")
|
||||
}
|
||||
|
||||
func TestGlobal_Signal(t *testing.T) {
|
||||
svc, _ := newTestService(t)
|
||||
|
||||
old := defaultService.Swap(svc)
|
||||
defer func() {
|
||||
if old != nil {
|
||||
defaultService.Store(old)
|
||||
}
|
||||
}()
|
||||
|
||||
proc, err := Start(context.Background(), "sleep", "60")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = Signal(proc.ID, syscall.SIGTERM)
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case <-proc.Done():
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("process should have been signalled through the global helper")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobal_SignalPID(t *testing.T) {
|
||||
svc, _ := newTestService(t)
|
||||
|
||||
old := defaultService.Swap(svc)
|
||||
defer func() {
|
||||
if old != nil {
|
||||
defaultService.Store(old)
|
||||
}
|
||||
}()
|
||||
|
||||
cmd := exec.Command("sleep", "60")
|
||||
require.NoError(t, cmd.Start())
|
||||
|
||||
waitCh := make(chan error, 1)
|
||||
go func() {
|
||||
waitCh <- cmd.Wait()
|
||||
}()
|
||||
|
||||
t.Cleanup(func() {
|
||||
if cmd.ProcessState == nil && cmd.Process != nil {
|
||||
_ = cmd.Process.Kill()
|
||||
}
|
||||
select {
|
||||
case <-waitCh:
|
||||
case <-time.After(2 * time.Second):
|
||||
}
|
||||
})
|
||||
|
||||
err := SignalPID(cmd.Process.Pid, syscall.SIGTERM)
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case err := <-waitCh:
|
||||
require.Error(t, err)
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("unmanaged process should have been signalled through the global helper")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobal_Running(t *testing.T) {
|
||||
svc, _ := newTestService(t)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package process
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
|
|
@ -152,6 +153,32 @@ func KillPID(pid int) error {
|
|||
return svc.KillPID(pid)
|
||||
}
|
||||
|
||||
// Signal sends a signal to a process by ID using the default service.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// _ = process.Signal("proc-1", syscall.SIGTERM)
|
||||
func Signal(id string, sig os.Signal) error {
|
||||
svc := Default()
|
||||
if svc == nil {
|
||||
return ErrServiceNotInitialized
|
||||
}
|
||||
return svc.Signal(id, sig)
|
||||
}
|
||||
|
||||
// SignalPID sends a signal to a process by operating-system PID using the default service.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// _ = process.SignalPID(1234, syscall.SIGTERM)
|
||||
func SignalPID(pid int, sig os.Signal) error {
|
||||
svc := Default()
|
||||
if svc == nil {
|
||||
return ErrServiceNotInitialized
|
||||
}
|
||||
return svc.SignalPID(pid, sig)
|
||||
}
|
||||
|
||||
// StartWithOptions spawns a process with full configuration using the default service.
|
||||
//
|
||||
// Example:
|
||||
|
|
|
|||
59
service.go
59
service.go
|
|
@ -5,6 +5,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"sync"
|
||||
|
|
@ -438,6 +439,45 @@ func (s *Service) KillPID(pid int) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Signal sends a signal to a process by ID.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// _ = svc.Signal("proc-1", syscall.SIGTERM)
|
||||
func (s *Service) Signal(id string, sig os.Signal) error {
|
||||
proc, err := s.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return proc.Signal(sig)
|
||||
}
|
||||
|
||||
// SignalPID sends a signal to a process by operating-system PID.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// _ = svc.SignalPID(1234, syscall.SIGTERM)
|
||||
func (s *Service) SignalPID(pid int, sig os.Signal) error {
|
||||
if pid <= 0 {
|
||||
return coreerr.E("Service.SignalPID", "pid must be positive", nil)
|
||||
}
|
||||
|
||||
if proc := s.findByPID(pid); proc != nil {
|
||||
return proc.Signal(sig)
|
||||
}
|
||||
|
||||
target, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
return coreerr.E("Service.SignalPID", fmt.Sprintf("failed to find pid %d", pid), err)
|
||||
}
|
||||
|
||||
if err := target.Signal(sig); err != nil {
|
||||
return coreerr.E("Service.SignalPID", fmt.Sprintf("failed to signal pid %d", pid), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove removes a completed process from the list.
|
||||
//
|
||||
// Example:
|
||||
|
|
@ -602,6 +642,25 @@ func (s *Service) handleTask(c *core.Core, task core.Task) core.Result {
|
|||
default:
|
||||
return core.Result{Value: coreerr.E("Service.handleTask", "task process kill requires an id or pid", nil), OK: false}
|
||||
}
|
||||
case TaskProcessSignal:
|
||||
if m.Signal == 0 {
|
||||
return core.Result{Value: coreerr.E("Service.handleTask", "task process signal requires a signal", nil), OK: false}
|
||||
}
|
||||
|
||||
switch {
|
||||
case m.ID != "":
|
||||
if err := s.Signal(m.ID, m.Signal); err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
return core.Result{OK: true}
|
||||
case m.PID > 0:
|
||||
if err := s.SignalPID(m.PID, m.Signal); err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
return core.Result{OK: true}
|
||||
default:
|
||||
return core.Result{Value: coreerr.E("Service.handleTask", "task process signal requires an id or pid", nil), OK: false}
|
||||
}
|
||||
case TaskProcessGet:
|
||||
if m.ID == "" {
|
||||
return core.Result{Value: coreerr.E("Service.handleTask", "task process get requires an id", nil), OK: false}
|
||||
|
|
|
|||
|
|
@ -518,6 +518,64 @@ func TestService_KillPID(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestService_Signal(t *testing.T) {
|
||||
t.Run("signals running process by id", func(t *testing.T) {
|
||||
svc, _ := newTestService(t)
|
||||
|
||||
proc, err := svc.Start(context.Background(), "sleep", "60")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = svc.Signal(proc.ID, syscall.SIGTERM)
|
||||
assert.NoError(t, err)
|
||||
|
||||
select {
|
||||
case <-proc.Done():
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("process should have been signalled")
|
||||
}
|
||||
|
||||
assert.Equal(t, StatusKilled, proc.Status)
|
||||
})
|
||||
|
||||
t.Run("signals unmanaged process by pid", func(t *testing.T) {
|
||||
svc, _ := newTestService(t)
|
||||
|
||||
cmd := exec.Command("sleep", "60")
|
||||
require.NoError(t, cmd.Start())
|
||||
|
||||
waitCh := make(chan error, 1)
|
||||
go func() {
|
||||
waitCh <- cmd.Wait()
|
||||
}()
|
||||
|
||||
t.Cleanup(func() {
|
||||
if cmd.ProcessState == nil && cmd.Process != nil {
|
||||
_ = cmd.Process.Kill()
|
||||
}
|
||||
select {
|
||||
case <-waitCh:
|
||||
case <-time.After(2 * time.Second):
|
||||
}
|
||||
})
|
||||
|
||||
err := svc.SignalPID(cmd.Process.Pid, syscall.SIGTERM)
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case err := <-waitCh:
|
||||
require.Error(t, err)
|
||||
var exitErr *exec.ExitError
|
||||
require.ErrorAs(t, err, &exitErr)
|
||||
ws, ok := exitErr.Sys().(syscall.WaitStatus)
|
||||
require.True(t, ok)
|
||||
assert.True(t, ws.Signaled())
|
||||
assert.Equal(t, syscall.SIGTERM, ws.Signal())
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("unmanaged process should have been signalled")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestService_Output(t *testing.T) {
|
||||
t.Run("returns captured output", func(t *testing.T) {
|
||||
svc, _ := newTestService(t)
|
||||
|
|
@ -688,6 +746,30 @@ func TestService_OnStartup(t *testing.T) {
|
|||
assert.NotEmpty(t, killed[0].Signal)
|
||||
})
|
||||
|
||||
t.Run("registers process.signal task", func(t *testing.T) {
|
||||
svc, c := newTestService(t)
|
||||
|
||||
err := svc.OnStartup(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
proc, err := svc.Start(context.Background(), "sleep", "60")
|
||||
require.NoError(t, err)
|
||||
|
||||
result := c.PERFORM(TaskProcessSignal{
|
||||
ID: proc.ID,
|
||||
Signal: syscall.SIGTERM,
|
||||
})
|
||||
require.True(t, result.OK)
|
||||
|
||||
select {
|
||||
case <-proc.Done():
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("process should have been signalled through core")
|
||||
}
|
||||
|
||||
assert.Equal(t, StatusKilled, proc.Status)
|
||||
})
|
||||
|
||||
t.Run("registers process.list task", func(t *testing.T) {
|
||||
svc, c := newTestService(t)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue