Add PID-based process kill support

This commit is contained in:
Virgil 2026-04-04 00:28:15 +00:00
parent ce2a4db6cb
commit 24f853631d
5 changed files with 70 additions and 0 deletions

View file

@ -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

View file

@ -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)

View file

@ -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()

View file

@ -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{}
}

View file

@ -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) {