diff --git a/actions.go b/actions.go index a93c9db..5341eac 100644 --- a/actions.go +++ b/actions.go @@ -23,6 +23,14 @@ type TaskProcessRun struct { KillGroup bool } +// TaskProcessKill requests termination of a managed process by ID or PID. +type TaskProcessKill struct { + // ID identifies a managed process started by this service. + ID string + // PID targets a process directly when ID is not available. + PID int +} + // ActionProcessStarted is broadcast when a process begins execution. type ActionProcessStarted struct { ID string diff --git a/global_test.go b/global_test.go index 975d682..4b07f88 100644 --- a/global_test.go +++ b/global_test.go @@ -36,6 +36,9 @@ func TestGlobal_DefaultNotInitialized(t *testing.T) { err = Kill("proc-1") assert.ErrorIs(t, err, ErrServiceNotInitialized) + err = KillPID(1234) + assert.ErrorIs(t, err, ErrServiceNotInitialized) + _, err = StartWithOptions(context.Background(), RunOptions{Command: "echo"}) assert.ErrorIs(t, err, ErrServiceNotInitialized) diff --git a/process_global.go b/process_global.go index 041fe4d..74434e4 100644 --- a/process_global.go +++ b/process_global.go @@ -94,6 +94,15 @@ func Kill(id string) error { return svc.Kill(id) } +// KillPID terminates a process by operating-system PID using the default service. +func KillPID(pid int) error { + svc := Default() + if svc == nil { + return ErrServiceNotInitialized + } + return svc.KillPID(pid) +} + // StartWithOptions spawns a process with full configuration using the default service. func StartWithOptions(ctx context.Context, opts RunOptions) (*Process, error) { svc := Default() diff --git a/service.go b/service.go index 1bca64b..ea4ebc7 100644 --- a/service.go +++ b/service.go @@ -338,6 +338,19 @@ func (s *Service) Kill(id string) error { return proc.Kill() } +// KillPID terminates a process by operating-system PID. +func (s *Service) KillPID(pid int) error { + if pid <= 0 { + return coreerr.E("Service.KillPID", "pid must be positive", nil) + } + + if err := syscall.Kill(pid, syscall.SIGTERM); err != nil { + return coreerr.E("Service.KillPID", fmt.Sprintf("failed to signal pid %d", pid), err) + } + + return nil +} + // Remove removes a completed process from the list. func (s *Service) Remove(id string) error { s.mu.Lock() @@ -435,6 +448,21 @@ func (s *Service) handleTask(c *core.Core, task core.Task) core.Result { return core.Result{Value: err, OK: false} } return core.Result{Value: output, OK: true} + case TaskProcessKill: + switch { + case m.ID != "": + if err := s.Kill(m.ID); err != nil { + return core.Result{Value: err, OK: false} + } + return core.Result{OK: true} + case m.PID > 0: + if err := s.KillPID(m.PID); 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 kill requires an id or pid", nil), OK: false} + } default: return core.Result{} } diff --git a/service_test.go b/service_test.go index f477fab..bdceddc 100644 --- a/service_test.go +++ b/service_test.go @@ -525,6 +525,28 @@ func TestService_OnStartup(t *testing.T) { require.False(t, result.OK) assert.Nil(t, result.Value) }) + + t.Run("registers process.kill 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) + require.True(t, proc.IsRunning()) + + result := c.PERFORM(TaskProcessKill{PID: proc.Info().PID}) + require.True(t, result.OK) + + select { + case <-proc.Done(): + case <-time.After(2 * time.Second): + t.Fatal("process should have been killed by pid") + } + + assert.Equal(t, StatusKilled, proc.Status) + }) } func TestService_RunWithOptions(t *testing.T) {