feat(03-02): integrate hook server into daemon with graceful degradation and dynamic poll
- Add hookPort, httpServer, lastHookTime, mu to Daemon struct - startHookServer binds HTTP on hookPort, degrades if port busy - Stop() closes httpServer before unix listener - pollLoop uses time.After(currentPollInterval()) instead of fixed ticker - currentPollInterval returns 20s when hooks active (<60s), 5s otherwise - processHookEvent records lastHookTime on each hook event Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
65
daemon.go
65
daemon.go
@@ -3,7 +3,9 @@ package main
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -155,6 +157,10 @@ type Daemon struct {
|
|||||||
pollInterval time.Duration
|
pollInterval time.Duration
|
||||||
stopCh chan struct{}
|
stopCh chan struct{}
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
|
hookPort int
|
||||||
|
httpServer *http.Server
|
||||||
|
lastHookTime time.Time
|
||||||
|
mu sync.Mutex // protects lastHookTime
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDaemon creates a daemon ready to start.
|
// NewDaemon creates a daemon ready to start.
|
||||||
@@ -167,6 +173,7 @@ func NewDaemon(sockPath, procDir, claudeDir string, labels *LabelStore) *Daemon
|
|||||||
claudeDir: claudeDir,
|
claudeDir: claudeDir,
|
||||||
pollInterval: 5 * time.Second,
|
pollInterval: 5 * time.Second,
|
||||||
stopCh: make(chan struct{}),
|
stopCh: make(chan struct{}),
|
||||||
|
hookPort: 3119,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,12 +192,60 @@ func (d *Daemon) InitWorkspaceResolver(treeProvider I3TreeProvider, x11 X11PIDRe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start runs the daemon: initial scan, then listens on the Unix socket
|
// startHookServer starts an HTTP server on hookPort to receive Claude Code hooks.
|
||||||
|
// If the port is busy, logs a warning and returns nil (graceful degradation).
|
||||||
|
func (d *Daemon) startHookServer() error {
|
||||||
|
if d.hookPort == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := fmt.Sprintf("127.0.0.1:%d", d.hookPort)
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/hook", d.handleHook)
|
||||||
|
|
||||||
|
d.httpServer = &http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Handler: mux,
|
||||||
|
ReadTimeout: 5 * time.Second,
|
||||||
|
WriteTimeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
ln, err := net.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("hook server: port %d busy, continuing without hooks: %v", d.hookPort, err)
|
||||||
|
d.httpServer = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
go d.httpServer.Serve(ln)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// currentPollInterval returns 20s when hooks are active (last hook < 60s ago),
|
||||||
|
// 5s otherwise.
|
||||||
|
func (d *Daemon) currentPollInterval() time.Duration {
|
||||||
|
d.mu.Lock()
|
||||||
|
lastHook := d.lastHookTime
|
||||||
|
d.mu.Unlock()
|
||||||
|
|
||||||
|
if !lastHook.IsZero() && time.Since(lastHook) < 60*time.Second {
|
||||||
|
return 20 * time.Second
|
||||||
|
}
|
||||||
|
return 5 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start runs the daemon: initial scan, hook server, then listens on the Unix socket
|
||||||
// and polls for sessions in the background.
|
// and polls for sessions in the background.
|
||||||
func (d *Daemon) Start() error {
|
func (d *Daemon) Start() error {
|
||||||
// Synchronous initial scan before accepting connections
|
// Synchronous initial scan before accepting connections
|
||||||
d.scanOnce(time.Now())
|
d.scanOnce(time.Now())
|
||||||
|
|
||||||
|
// Start hook server (graceful degradation if port busy)
|
||||||
|
if err := d.startHookServer(); err != nil {
|
||||||
|
return fmt.Errorf("start hook server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := d.cleanStaleSocket(); err != nil {
|
if err := d.cleanStaleSocket(); err != nil {
|
||||||
return fmt.Errorf("clean stale socket: %w", err)
|
return fmt.Errorf("clean stale socket: %w", err)
|
||||||
}
|
}
|
||||||
@@ -218,6 +273,9 @@ func (d *Daemon) Stop() {
|
|||||||
default:
|
default:
|
||||||
close(d.stopCh)
|
close(d.stopCh)
|
||||||
}
|
}
|
||||||
|
if d.httpServer != nil {
|
||||||
|
d.httpServer.Close()
|
||||||
|
}
|
||||||
if d.listener != nil {
|
if d.listener != nil {
|
||||||
d.listener.Close()
|
d.listener.Close()
|
||||||
}
|
}
|
||||||
@@ -244,14 +302,11 @@ func (d *Daemon) acceptLoop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Daemon) pollLoop() {
|
func (d *Daemon) pollLoop() {
|
||||||
ticker := time.NewTicker(d.pollInterval)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-d.stopCh:
|
case <-d.stopCh:
|
||||||
return
|
return
|
||||||
case t := <-ticker.C:
|
case t := <-time.After(d.currentPollInterval()):
|
||||||
d.scanOnce(t)
|
d.scanOnce(t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
hook.go
4
hook.go
@@ -71,6 +71,10 @@ func (d *Daemon) processHookEvent(event HookEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
d.registry.UpdateFromHook(event.SessionID, state, waitType, event.Cwd)
|
d.registry.UpdateFromHook(event.SessionID, state, waitType, event.Cwd)
|
||||||
|
|
||||||
|
d.mu.Lock()
|
||||||
|
d.lastHookTime = time.Now()
|
||||||
|
d.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateFromHook updates a session from a hook event.
|
// UpdateFromHook updates a session from a hook event.
|
||||||
|
|||||||
Reference in New Issue
Block a user