feat(process): track killed process lifecycle
This commit is contained in:
parent
0546b42ce3
commit
b6530cf85d
3 changed files with 100 additions and 34 deletions
|
|
@ -143,6 +143,8 @@ func TestProcess_Kill(t *testing.T) {
|
|||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("process should have been killed")
|
||||
}
|
||||
|
||||
assert.Equal(t, StatusKilled, proc.Status)
|
||||
})
|
||||
|
||||
t.Run("noop on completed process", func(t *testing.T) {
|
||||
|
|
@ -209,6 +211,8 @@ func TestProcess_Signal(t *testing.T) {
|
|||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("process should have been terminated by signal")
|
||||
}
|
||||
|
||||
assert.Equal(t, StatusKilled, proc.Status)
|
||||
})
|
||||
|
||||
t.Run("error on completed process", func(t *testing.T) {
|
||||
|
|
@ -279,6 +283,7 @@ func TestProcess_Timeout(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.False(t, proc.IsRunning())
|
||||
assert.Equal(t, StatusKilled, proc.Status)
|
||||
})
|
||||
|
||||
t.Run("no timeout when zero", func(t *testing.T) {
|
||||
|
|
@ -319,6 +324,8 @@ func TestProcess_Shutdown(t *testing.T) {
|
|||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("shutdown should have completed")
|
||||
}
|
||||
|
||||
assert.Equal(t, StatusKilled, proc.Status)
|
||||
})
|
||||
|
||||
t.Run("immediate kill without grace period", func(t *testing.T) {
|
||||
|
|
@ -367,6 +374,8 @@ func TestProcess_KillGroup(t *testing.T) {
|
|||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("process group should have been killed")
|
||||
}
|
||||
|
||||
assert.Equal(t, StatusKilled, proc.Status)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -388,5 +397,7 @@ func TestProcess_TimeoutWithGrace(t *testing.T) {
|
|||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("process should have been killed by timeout")
|
||||
}
|
||||
|
||||
assert.Equal(t, StatusKilled, proc.Status)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
79
service.go
79
service.go
|
|
@ -220,38 +220,31 @@ func (s *Service) StartWithOptions(ctx context.Context, opts RunOptions) (*Proce
|
|||
err := cmd.Wait()
|
||||
|
||||
duration := time.Since(proc.StartedAt)
|
||||
status, exitCode, exitErr, signalName := classifyProcessExit(err)
|
||||
|
||||
proc.mu.Lock()
|
||||
proc.Duration = duration
|
||||
if err != nil {
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(err, &exitErr) {
|
||||
proc.ExitCode = exitErr.ExitCode()
|
||||
proc.Status = StatusExited
|
||||
} else {
|
||||
proc.Status = StatusFailed
|
||||
}
|
||||
} else {
|
||||
proc.ExitCode = 0
|
||||
proc.Status = StatusExited
|
||||
}
|
||||
status := proc.Status
|
||||
exitCode := proc.ExitCode
|
||||
proc.ExitCode = exitCode
|
||||
proc.Status = status
|
||||
proc.mu.Unlock()
|
||||
|
||||
close(proc.done)
|
||||
|
||||
// Broadcast exit
|
||||
var exitErr error
|
||||
if status == StatusFailed {
|
||||
exitErr = err
|
||||
// Broadcast lifecycle completion.
|
||||
switch status {
|
||||
case StatusKilled:
|
||||
_ = s.Core().ACTION(ActionProcessKilled{
|
||||
ID: id,
|
||||
Signal: signalName,
|
||||
})
|
||||
default:
|
||||
_ = s.Core().ACTION(ActionProcessExited{
|
||||
ID: id,
|
||||
ExitCode: exitCode,
|
||||
Duration: duration,
|
||||
Error: exitErr,
|
||||
})
|
||||
}
|
||||
_ = s.Core().ACTION(ActionProcessExited{
|
||||
ID: id,
|
||||
ExitCode: exitCode,
|
||||
Duration: duration,
|
||||
Error: exitErr,
|
||||
})
|
||||
}()
|
||||
|
||||
return proc, nil
|
||||
|
|
@ -325,16 +318,7 @@ func (s *Service) Kill(id string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if err := proc.Kill(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = s.Core().ACTION(ActionProcessKilled{
|
||||
ID: id,
|
||||
Signal: "SIGKILL",
|
||||
})
|
||||
|
||||
return nil
|
||||
return proc.Kill()
|
||||
}
|
||||
|
||||
// Remove removes a completed process from the list.
|
||||
|
|
@ -387,6 +371,9 @@ func (s *Service) Run(ctx context.Context, command string, args ...string) (stri
|
|||
<-proc.Done()
|
||||
|
||||
output := proc.Output()
|
||||
if proc.Status == StatusKilled {
|
||||
return output, coreerr.E("Service.Run", "process was killed", nil)
|
||||
}
|
||||
if proc.ExitCode != 0 {
|
||||
return output, coreerr.E("Service.Run", fmt.Sprintf("process exited with code %d", proc.ExitCode), nil)
|
||||
}
|
||||
|
|
@ -403,6 +390,9 @@ func (s *Service) RunWithOptions(ctx context.Context, opts RunOptions) (string,
|
|||
<-proc.Done()
|
||||
|
||||
output := proc.Output()
|
||||
if proc.Status == StatusKilled {
|
||||
return output, coreerr.E("Service.RunWithOptions", "process was killed", nil)
|
||||
}
|
||||
if proc.ExitCode != 0 {
|
||||
return output, coreerr.E("Service.RunWithOptions", fmt.Sprintf("process exited with code %d", proc.ExitCode), nil)
|
||||
}
|
||||
|
|
@ -427,3 +417,24 @@ func (s *Service) handleTask(c *core.Core, task core.Task) core.Result {
|
|||
return core.Result{}
|
||||
}
|
||||
}
|
||||
|
||||
// classifyProcessExit maps a command completion error to lifecycle state.
|
||||
func classifyProcessExit(err error) (Status, int, error, string) {
|
||||
if err == nil {
|
||||
return StatusExited, 0, nil, ""
|
||||
}
|
||||
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(err, &exitErr) {
|
||||
if ws, ok := exitErr.Sys().(syscall.WaitStatus); ok && ws.Signaled() {
|
||||
signalName := ws.Signal().String()
|
||||
if signalName == "" {
|
||||
signalName = "signal"
|
||||
}
|
||||
return StatusKilled, -1, nil, signalName
|
||||
}
|
||||
return StatusExited, exitErr.ExitCode(), nil, ""
|
||||
}
|
||||
|
||||
return StatusFailed, 0, err, ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -226,6 +226,48 @@ func TestService_Actions(t *testing.T) {
|
|||
assert.Len(t, exited, 1)
|
||||
assert.Equal(t, 0, exited[0].ExitCode)
|
||||
})
|
||||
|
||||
t.Run("broadcasts killed events", func(t *testing.T) {
|
||||
c := framework.New()
|
||||
|
||||
factory := NewService(Options{})
|
||||
raw, err := factory(c)
|
||||
require.NoError(t, err)
|
||||
svc := raw.(*Service)
|
||||
|
||||
var killed []ActionProcessKilled
|
||||
var mu sync.Mutex
|
||||
|
||||
c.RegisterAction(func(cc *framework.Core, msg framework.Message) framework.Result {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if m, ok := msg.(ActionProcessKilled); ok {
|
||||
killed = append(killed, m)
|
||||
}
|
||||
return framework.Result{OK: true}
|
||||
})
|
||||
|
||||
proc, err := svc.Start(context.Background(), "sleep", "60")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = svc.Kill(proc.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case <-proc.Done():
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("process should have been killed")
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
assert.Len(t, killed, 1)
|
||||
assert.Equal(t, proc.ID, killed[0].ID)
|
||||
assert.NotEmpty(t, killed[0].Signal)
|
||||
assert.Equal(t, StatusKilled, proc.Status)
|
||||
})
|
||||
}
|
||||
|
||||
func TestService_List(t *testing.T) {
|
||||
|
|
@ -328,6 +370,8 @@ func TestService_Kill(t *testing.T) {
|
|||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("process should have been killed")
|
||||
}
|
||||
|
||||
assert.Equal(t, StatusKilled, proc.Status)
|
||||
})
|
||||
|
||||
t.Run("error on unknown id", func(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue