diff --git a/service.go b/service.go index b9fbf1c..10d086d 100644 --- a/service.go +++ b/service.go @@ -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 diff --git a/service_test.go b/service_test.go index 7c6b640..f0aeff1 100644 --- a/service_test.go +++ b/service_test.go @@ -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) {