feat(process): emit exit actions consistently

This commit is contained in:
Virgil 2026-04-03 23:37:48 +00:00
parent f70e301631
commit dcf058047e
2 changed files with 59 additions and 11 deletions

View file

@ -103,6 +103,7 @@ func (s *Service) Start(ctx context.Context, command string, args ...string) (*P
// StartWithOptions spawns a process with full configuration.
func (s *Service) StartWithOptions(ctx context.Context, opts RunOptions) (*Process, error) {
id := fmt.Sprintf("proc-%d", s.idCounter.Add(1))
startedAt := time.Now()
if opts.KillGroup && !opts.Detach {
return nil, coreerr.E("Service.StartWithOptions", "KillGroup requires Detach", nil)
@ -159,7 +160,7 @@ func (s *Service) StartWithOptions(ctx context.Context, opts RunOptions) (*Proce
Args: opts.Args,
Dir: opts.Dir,
Env: opts.Env,
StartedAt: time.Now(),
StartedAt: startedAt,
Status: StatusRunning,
cmd: cmd,
ctx: procCtx,
@ -174,6 +175,14 @@ func (s *Service) StartWithOptions(ctx context.Context, opts RunOptions) (*Proce
// Start the process
if err := cmd.Start(); err != nil {
cancel()
if s.Core() != nil {
_ = s.Core().ACTION(ActionProcessExited{
ID: id,
ExitCode: -1,
Duration: time.Since(startedAt),
Error: coreerr.E("Service.StartWithOptions", "failed to start process", err),
})
}
return nil, coreerr.E("Service.StartWithOptions", "failed to start process", err)
}
@ -234,21 +243,21 @@ func (s *Service) StartWithOptions(ctx context.Context, opts RunOptions) (*Proce
close(proc.done)
// Broadcast lifecycle completion.
switch status {
case StatusKilled:
exitAction := ActionProcessExited{
ID: id,
ExitCode: exitCode,
Duration: duration,
Error: exitErr,
}
if status == StatusKilled {
exitAction.Error = coreerr.E("Service.StartWithOptions", "process was killed", nil)
_ = s.Core().ACTION(ActionProcessKilled{
ID: id,
Signal: signalName,
})
default:
_ = s.Core().ACTION(ActionProcessExited{
ID: id,
ExitCode: exitCode,
Duration: duration,
Error: exitErr,
})
}
_ = s.Core().ACTION(exitAction)
}()
return proc, nil

View file

@ -248,6 +248,7 @@ func TestService_Actions(t *testing.T) {
svc := raw.(*Service)
var killed []ActionProcessKilled
var exited []ActionProcessExited
var mu sync.Mutex
c.RegisterAction(func(cc *framework.Core, msg framework.Message) framework.Result {
@ -256,6 +257,9 @@ func TestService_Actions(t *testing.T) {
if m, ok := msg.(ActionProcessKilled); ok {
killed = append(killed, m)
}
if m, ok := msg.(ActionProcessExited); ok {
exited = append(exited, m)
}
return framework.Result{OK: true}
})
@ -278,8 +282,43 @@ func TestService_Actions(t *testing.T) {
assert.Len(t, killed, 1)
assert.Equal(t, proc.ID, killed[0].ID)
assert.NotEmpty(t, killed[0].Signal)
assert.Len(t, exited, 1)
assert.Equal(t, proc.ID, exited[0].ID)
assert.Error(t, exited[0].Error)
assert.Equal(t, StatusKilled, proc.Status)
})
t.Run("broadcasts exited event on start failure", func(t *testing.T) {
c := framework.New()
factory := NewService(Options{})
raw, err := factory(c)
require.NoError(t, err)
svc := raw.(*Service)
var exited []ActionProcessExited
var mu sync.Mutex
c.RegisterAction(func(cc *framework.Core, msg framework.Message) framework.Result {
mu.Lock()
defer mu.Unlock()
if m, ok := msg.(ActionProcessExited); ok {
exited = append(exited, m)
}
return framework.Result{OK: true}
})
_, err = svc.Start(context.Background(), "definitely-not-a-real-binary-xyz")
require.Error(t, err)
time.Sleep(10 * time.Millisecond)
mu.Lock()
defer mu.Unlock()
require.Len(t, exited, 1)
assert.Equal(t, -1, exited[0].ExitCode)
assert.Error(t, exited[0].Error)
})
}
func TestService_List(t *testing.T) {