Skip to content

Commit 12bed1c

Browse files
committed
feat: implement platform-specific signal handling
1 parent 30f82d7 commit 12bed1c

3 files changed

Lines changed: 89 additions & 51 deletions

File tree

lib/httpapi/server.go

Lines changed: 21 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,11 @@ import (
1111
"net/http"
1212
"net/url"
1313
"os"
14-
"os/signal"
1514
"path/filepath"
1615
"slices"
1716
"sort"
1817
"strings"
1918
"sync"
20-
"syscall"
2119
"time"
2220
"unicode"
2321

@@ -672,59 +670,31 @@ func (s *Server) cleanupPIDFile() {
672670
}
673671
}
674672

675-
// HandleSignals sets up signal handlers for:
676-
// - SIGTERM, SIGINT, SIGHUP: save conversation state, then close the process
677-
// - SIGUSR1: save conversation state without exiting
678-
func (s *Server) HandleSignals(ctx context.Context, process *termexec.Process) {
679-
// Handle shutdown signals (SIGTERM, SIGINT, SIGHUP)
680-
shutdownCh := make(chan os.Signal, 1)
681-
signal.Notify(shutdownCh, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
682-
go func() {
683-
sig := <-shutdownCh
684-
s.logger.Info("Received shutdown signal, saving state before closing process", "signal", sig)
685-
686-
// Save conversation state if configured (synchronously before closing process)
687-
if s.statePersistenceCfg.SaveState && s.statePersistenceCfg.StateFile != "" {
688-
if err := s.conversation.SaveState(s.conversation.Messages(), s.statePersistenceCfg.StateFile); err != nil {
689-
s.logger.Error("Failed to save conversation state on signal", "signal", sig, "error", err)
690-
} else {
691-
s.logger.Info("Saved conversation state on signal", "signal", sig, "stateFile", s.statePersistenceCfg.StateFile)
692-
}
693-
}
673+
// saveAndCleanup saves the conversation state and cleans up before shutdown
674+
func (s *Server) saveAndCleanup(sig os.Signal, process *termexec.Process) {
675+
// Save conversation state if configured (synchronously before closing process)
676+
s.saveStateIfConfigured(sig.String())
694677

695-
// Clean up PID file
696-
s.cleanupPIDFile()
678+
// Clean up PID file
679+
s.cleanupPIDFile()
697680

698-
// Now close the process
699-
if err := process.Close(s.logger, 5*time.Second); err != nil {
700-
s.logger.Error("Error closing process", "signal", sig, "error", err)
701-
}
702-
}()
681+
// Now close the process
682+
if err := process.Close(s.logger, 5*time.Second); err != nil {
683+
s.logger.Error("Error closing process", "signal", sig, "error", err)
684+
}
685+
}
703686

704-
// Handle SIGUSR1 for save without exit
705-
saveOnlyCh := make(chan os.Signal, 1)
706-
signal.Notify(saveOnlyCh, syscall.SIGUSR1)
707-
go func() {
708-
for {
709-
select {
710-
case <-saveOnlyCh:
711-
s.logger.Info("Received SIGUSR1, saving state without exiting")
712-
713-
// Save conversation state if configured
714-
if s.statePersistenceCfg.SaveState && s.statePersistenceCfg.StateFile != "" {
715-
if err := s.conversation.SaveState(s.conversation.Messages(), s.statePersistenceCfg.StateFile); err != nil {
716-
s.logger.Error("Failed to save conversation state on SIGUSR1", "error", err)
717-
} else {
718-
s.logger.Info("Saved conversation state on SIGUSR1", "stateFile", s.statePersistenceCfg.StateFile)
719-
}
720-
} else {
721-
s.logger.Warn("SIGUSR1 received but state saving is not configured")
722-
}
723-
case <-ctx.Done():
724-
return
725-
}
687+
// saveStateIfConfigured saves the conversation state if configured
688+
func (s *Server) saveStateIfConfigured(source string) {
689+
if s.statePersistenceCfg.SaveState && s.statePersistenceCfg.StateFile != "" {
690+
if err := s.conversation.SaveState(s.conversation.Messages(), s.statePersistenceCfg.StateFile); err != nil {
691+
s.logger.Error("Failed to save conversation state", "source", source, "error", err)
692+
} else {
693+
s.logger.Info("Saved conversation state", "source", source, "stateFile", s.statePersistenceCfg.StateFile)
726694
}
727-
}()
695+
} else {
696+
s.logger.Warn("Save requested but state saving is not configured", "source", source)
697+
}
728698
}
729699

730700
// registerStaticFileRoutes sets up routes for serving static files

lib/httpapi/server_signals_unix.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//go:build unix
2+
3+
package httpapi
4+
5+
import (
6+
"context"
7+
"os"
8+
"os/signal"
9+
"syscall"
10+
11+
"github.com/coder/agentapi/lib/termexec"
12+
)
13+
14+
// HandleSignals sets up signal handlers for:
15+
// - SIGTERM, SIGINT, SIGHUP: save conversation state, then close the process
16+
// - SIGUSR1: save conversation state without exiting
17+
func (s *Server) HandleSignals(ctx context.Context, process *termexec.Process) {
18+
// Handle shutdown signals (SIGTERM, SIGINT, SIGHUP)
19+
shutdownCh := make(chan os.Signal, 1)
20+
signal.Notify(shutdownCh, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
21+
go func() {
22+
sig := <-shutdownCh
23+
s.logger.Info("Received shutdown signal, saving state before closing process", "signal", sig)
24+
25+
s.saveAndCleanup(sig, process)
26+
}()
27+
28+
// Handle SIGUSR1 for save without exit
29+
saveOnlyCh := make(chan os.Signal, 1)
30+
signal.Notify(saveOnlyCh, syscall.SIGUSR1)
31+
go func() {
32+
for {
33+
select {
34+
case <-saveOnlyCh:
35+
s.logger.Info("Received SIGUSR1, saving state without exiting")
36+
s.saveStateIfConfigured("SIGUSR1")
37+
case <-ctx.Done():
38+
return
39+
}
40+
}
41+
}()
42+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//go:build windows
2+
3+
package httpapi
4+
5+
import (
6+
"context"
7+
"os"
8+
"os/signal"
9+
"syscall"
10+
11+
"github.com/coder/agentapi/lib/termexec"
12+
)
13+
14+
// HandleSignals sets up signal handlers for Windows.
15+
// Only handles SIGTERM and SIGINT (SIGHUP and SIGUSR1 don't exist on Windows).
16+
func (s *Server) HandleSignals(ctx context.Context, process *termexec.Process) {
17+
// Handle shutdown signals (SIGTERM, SIGINT only on Windows)
18+
shutdownCh := make(chan os.Signal, 1)
19+
signal.Notify(shutdownCh, os.Interrupt, syscall.SIGTERM)
20+
go func() {
21+
sig := <-shutdownCh
22+
s.logger.Info("Received shutdown signal, saving state before closing process", "signal", sig)
23+
24+
s.saveAndCleanup(sig, process)
25+
}()
26+
}

0 commit comments

Comments
 (0)