diff --git a/service.go b/service.go index 4edf163..5c8d8b1 100644 --- a/service.go +++ b/service.go @@ -30,6 +30,7 @@ var ( ErrProcessNotRunning = coreerr.E("", "process is not running", nil) ErrStdinNotAvailable = coreerr.E("", "stdin not available", nil) ErrContextRequired = coreerr.E("", "context is required", nil) + ErrUncatchableSignal = coreerr.E("", "signal cannot be caught", nil) ) // Service manages process execution with Core IPC integration. @@ -458,6 +459,10 @@ func (s *Service) KillPID(pid int) error { // // _ = svc.Signal("proc-1", syscall.SIGTERM) func (s *Service) Signal(id string, sig os.Signal) error { + if err := validateCatchableSignals(sig); err != nil { + return err + } + proc, err := s.Get(id) if err != nil { return err @@ -471,6 +476,10 @@ func (s *Service) Signal(id string, sig os.Signal) error { // // _ = svc.SignalPID(1234, syscall.SIGTERM) func (s *Service) SignalPID(pid int, sig os.Signal) error { + if err := validateCatchableSignals(sig); err != nil { + return err + } + if pid <= 0 { return ServiceError("pid must be positive", nil) } @@ -491,6 +500,26 @@ func (s *Service) SignalPID(pid int, sig os.Signal) error { return nil } +func validateCatchableSignals(signals ...os.Signal) error { + for _, sig := range signals { + sysSig, ok := sig.(syscall.Signal) + if !ok { + continue + } + + switch sysSig { + case syscall.SIGKILL, syscall.SIGSTOP: + return coreerr.E( + "Service.validateCatchableSignals", + core.Sprintf("signal %d cannot be caught", int(sysSig)), + ErrUncatchableSignal, + ) + } + } + + return nil +} + // Remove removes a completed process from the list. // // Example: diff --git a/service_test.go b/service_test.go index ef60fab..39a5207 100644 --- a/service_test.go +++ b/service_test.go @@ -3,6 +3,7 @@ package process import ( "context" "os/exec" + "strconv" "strings" // Note: AX-6 — internal concurrency primitive; structural per RFC §2 "sync" @@ -590,6 +591,37 @@ func TestService_KillPID(t *testing.T) { } func TestService_Signal(t *testing.T) { + t.Run("rejects uncatchable signals", func(t *testing.T) { + svc, _ := newTestService(t) + + for _, tc := range []struct { + name string + send func(syscall.Signal) error + }{ + { + name: "by id", + send: func(sig syscall.Signal) error { + return svc.Signal("nonexistent", sig) + }, + }, + { + name: "by pid", + send: func(sig syscall.Signal) error { + return svc.SignalPID(999999, sig) + }, + }, + } { + for _, sig := range []syscall.Signal{syscall.SIGKILL, syscall.SIGSTOP} { + t.Run(tc.name+"/"+strconv.Itoa(int(sig)), func(t *testing.T) { + err := tc.send(sig) + requireError(t, err) + assertErrorIs(t, err, ErrUncatchableSignal) + assertContains(t, err.Error(), "signal "+strconv.Itoa(int(sig))+" cannot be caught") + }) + } + } + }) + t.Run("signals running process by id", func(t *testing.T) { svc, _ := newTestService(t)