-
Notifications
You must be signed in to change notification settings - Fork 121
cmd/wol-proxy: tweak logs to show what is causing wake ups #356
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
8a7b3ab
6d71b61
6cfbb28
2b04ddd
7164ea8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,7 @@ | ||||||||||||||||||||||||||||||||||
| package main | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||
| "bufio" | ||||||||||||||||||||||||||||||||||
| "context" | ||||||||||||||||||||||||||||||||||
| "errors" | ||||||||||||||||||||||||||||||||||
| "flag" | ||||||||||||||||||||||||||||||||||
|
|
@@ -13,6 +14,7 @@ import ( | |||||||||||||||||||||||||||||||||
| "net/url" | ||||||||||||||||||||||||||||||||||
| "os" | ||||||||||||||||||||||||||||||||||
| "os/signal" | ||||||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||||||
| "sync" | ||||||||||||||||||||||||||||||||||
| "time" | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
|
|
@@ -92,14 +94,9 @@ func main() { | |||||||||||||||||||||||||||||||||
| }() | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // graceful shutdown | ||||||||||||||||||||||||||||||||||
| ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) | ||||||||||||||||||||||||||||||||||
| defer stop() | ||||||||||||||||||||||||||||||||||
| ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt) | ||||||||||||||||||||||||||||||||||
| <-ctx.Done() | ||||||||||||||||||||||||||||||||||
| shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||||||||||||||||||||||||||||||||||
| defer cancel() | ||||||||||||||||||||||||||||||||||
| if err := server.Shutdown(shutdownCtx); err != nil { | ||||||||||||||||||||||||||||||||||
| slog.Error("server shutdown error", "error", err) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| server.Close() | ||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Immediate close terminates active connections abruptly.
Apply this diff: ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
+ defer stop()
<-ctx.Done()
- server.Close()
+ shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ if err := server.Shutdown(shutdownCtx); err != nil {
+ slog.Error("error during shutdown", "error", err)
+ }🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| type upstreamStatus string | ||||||||||||||||||||||||||||||||||
|
|
@@ -124,37 +121,88 @@ func newProxy(url *url.URL) *proxyServer { | |||||||||||||||||||||||||||||||||
| failCount: 0, | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // start a goroutien to check upstream status | ||||||||||||||||||||||||||||||||||
| // start a goroutine to monitor upstream status via SSE | ||||||||||||||||||||||||||||||||||
| go func() { | ||||||||||||||||||||||||||||||||||
| checkUrl := url.Scheme + "://" + url.Host + "/wol-health" | ||||||||||||||||||||||||||||||||||
| client := &http.Client{Timeout: time.Second} | ||||||||||||||||||||||||||||||||||
| ticker := time.NewTicker(2 * time.Second) | ||||||||||||||||||||||||||||||||||
| defer ticker.Stop() | ||||||||||||||||||||||||||||||||||
| for range ticker.C { | ||||||||||||||||||||||||||||||||||
| eventsUrl := url.Scheme + "://" + url.Host + "/api/events" | ||||||||||||||||||||||||||||||||||
| client := &http.Client{ | ||||||||||||||||||||||||||||||||||
| Timeout: 0, // No timeout for SSE connection | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| slog.Debug("checking upstream status at", "url", checkUrl) | ||||||||||||||||||||||||||||||||||
| resp, err := client.Get(checkUrl) | ||||||||||||||||||||||||||||||||||
| waitDuration := 10 * time.Second | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+127
to
+131
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SSE client can hang indefinitely on connect; set dial/TLS timeouts Timeout=0 is correct for the stream, but the default Transport has no dial or TLS handshake timeouts, so a dead upstream can block forever. - client := &http.Client{
- Timeout: 0, // No timeout for SSE connection
- }
+ tr := &http.Transport{
+ DialContext: (&net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
+ TLSHandshakeTimeout: 10 * time.Second,
+ IdleConnTimeout: 90 * time.Second,
+ MaxIdleConns: 100,
+ }
+ client := &http.Client{Transport: tr, Timeout: 0} // no overall timeout; stream is long‑lived📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // drain the body | ||||||||||||||||||||||||||||||||||
| if err == nil && resp != nil { | ||||||||||||||||||||||||||||||||||
| _, _ = io.Copy(io.Discard, resp.Body) | ||||||||||||||||||||||||||||||||||
| _ = resp.Body.Close() | ||||||||||||||||||||||||||||||||||
| for { | ||||||||||||||||||||||||||||||||||
| slog.Debug("connecting to SSE endpoint", "url", eventsUrl) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| req, err := http.NewRequest("GET", eventsUrl, nil) | ||||||||||||||||||||||||||||||||||
|
Comment on lines
126
to
136
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Build SSE URL robustly; avoid shadowing net/url; fix initialism String concat drops any upstream base path and the param name url shadows the net/url package; eventsUrl casing violates ST1003. - eventsUrl := url.Scheme + "://" + url.Host + "/api/events"
+ // Build from upstream, preserving base path.
+ eventsURL, err := neturl.JoinPath(upstream.String(), "api", "events")
+ if err != nil {
+ slog.Warn("failed to build SSE URL", "error", err)
+ proxy.setStatus(notready)
+ proxy.incFail()
+ time.Sleep(waitDuration)
+ continue
+ }Supporting changes outside this hunk:
Based on coding guidelines (staticcheck ST1003).
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||
| slog.Warn("failed to create SSE request", "error", err) | ||||||||||||||||||||||||||||||||||
| proxy.setStatus(notready) | ||||||||||||||||||||||||||||||||||
| proxy.failCount++ | ||||||||||||||||||||||||||||||||||
| time.Sleep(waitDuration) | ||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if err == nil && resp != nil && resp.StatusCode == http.StatusOK { | ||||||||||||||||||||||||||||||||||
| slog.Debug("upstream status: ready") | ||||||||||||||||||||||||||||||||||
| proxy.setStatus(ready) | ||||||||||||||||||||||||||||||||||
| proxy.statusMutex.Lock() | ||||||||||||||||||||||||||||||||||
| proxy.failCount = 0 | ||||||||||||||||||||||||||||||||||
| proxy.statusMutex.Unlock() | ||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||
| slog.Debug("upstream status: notready", "error", err) | ||||||||||||||||||||||||||||||||||
| req.Header.Set("Accept", "text/event-stream") | ||||||||||||||||||||||||||||||||||
| req.Header.Set("Cache-Control", "no-cache") | ||||||||||||||||||||||||||||||||||
| req.Header.Set("Connection", "keep-alive") | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| resp, err := client.Do(req) | ||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||
| slog.Error("failed to connect to SSE endpoint", "error", err) | ||||||||||||||||||||||||||||||||||
| proxy.setStatus(notready) | ||||||||||||||||||||||||||||||||||
| proxy.statusMutex.Lock() | ||||||||||||||||||||||||||||||||||
| proxy.failCount++ | ||||||||||||||||||||||||||||||||||
| proxy.statusMutex.Unlock() | ||||||||||||||||||||||||||||||||||
| time.Sleep(10 * time.Second) | ||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use waitDuration consistently for retry delays. These lines use hardcoded Apply this diff: - time.Sleep(10 * time.Second)
+ time.Sleep(waitDuration)Apply to both lines 156 and 166. Also applies to: 166-166 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if resp.StatusCode != http.StatusOK { | ||||||||||||||||||||||||||||||||||
| slog.Warn("SSE endpoint returned non-OK status", "status", resp.StatusCode) | ||||||||||||||||||||||||||||||||||
| _, _ = io.Copy(io.Discard, resp.Body) | ||||||||||||||||||||||||||||||||||
| _ = resp.Body.Close() | ||||||||||||||||||||||||||||||||||
| proxy.setStatus(notready) | ||||||||||||||||||||||||||||||||||
| proxy.failCount++ | ||||||||||||||||||||||||||||||||||
| time.Sleep(10 * time.Second) | ||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Successfully connected to SSE endpoint | ||||||||||||||||||||||||||||||||||
| slog.Info("connected to SSE endpoint, upstream ready") | ||||||||||||||||||||||||||||||||||
| proxy.setStatus(ready) | ||||||||||||||||||||||||||||||||||
| proxy.failCount = 0 | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Read from the SSE stream to detect disconnection | ||||||||||||||||||||||||||||||||||
| scanner := bufio.NewScanner(resp.Body) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // use a fairly large buffer to avoid scanner errors when reading large SSE events | ||||||||||||||||||||||||||||||||||
| buf := make([]byte, 0, 1024*1024*2) | ||||||||||||||||||||||||||||||||||
| scanner.Buffer(buf, 1024*1024*2) | ||||||||||||||||||||||||||||||||||
| events := 0 | ||||||||||||||||||||||||||||||||||
| if slog.Default().Enabled(context.Background(), slog.LevelDebug) { | ||||||||||||||||||||||||||||||||||
| fmt.Print("Events: ") | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| for scanner.Scan() { | ||||||||||||||||||||||||||||||||||
| if slog.Default().Enabled(context.Background(), slog.LevelDebug) { | ||||||||||||||||||||||||||||||||||
| // Just read the events to keep connection alive | ||||||||||||||||||||||||||||||||||
| // We don't need to process the event data | ||||||||||||||||||||||||||||||||||
| events++ | ||||||||||||||||||||||||||||||||||
| fmt.Printf("%d, ", events) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| fmt.Println() | ||||||||||||||||||||||||||||||||||
|
Comment on lines
180
to
191
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replace fmt debug output with structured slog logging. Using Apply this diff: events := 0
- if slog.Default().Enabled(context.Background(), slog.LevelDebug) {
- fmt.Print("Events: ")
- }
for scanner.Scan() {
- if slog.Default().Enabled(context.Background(), slog.LevelDebug) {
- // Just read the events to keep connection alive
- // We don't need to process the event data
- events++
- fmt.Printf("%d, ", events)
- }
+ events++
}
- fmt.Println()
+ slog.Debug("SSE stream ended", "events_received", events)🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| if err := scanner.Err(); err != nil { | ||||||||||||||||||||||||||||||||||
| slog.Error("error reading from SSE stream", "error", err) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Connection closed or error occurred | ||||||||||||||||||||||||||||||||||
| _ = resp.Body.Close() | ||||||||||||||||||||||||||||||||||
| slog.Info("SSE connection closed, upstream not ready") | ||||||||||||||||||||||||||||||||||
| proxy.setStatus(notready) | ||||||||||||||||||||||||||||||||||
| proxy.failCount++ | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Wait before reconnecting | ||||||||||||||||||||||||||||||||||
| time.Sleep(waitDuration) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| }() | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
@@ -175,7 +223,14 @@ func (p *proxyServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if p.getStatus() == notready { | ||||||||||||||||||||||||||||||||||
| slog.Info("upstream not ready, sending magic packet", "mac", *flagMac) | ||||||||||||||||||||||||||||||||||
| path := r.URL.Path | ||||||||||||||||||||||||||||||||||
| if strings.HasPrefix(path, "/api/events") { | ||||||||||||||||||||||||||||||||||
| slog.Debug("Skipping wake up", "req", path) | ||||||||||||||||||||||||||||||||||
| w.WriteHeader(http.StatusNoContent) | ||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+223
to
+227
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Path prefix check may match unintended paths.
Apply this diff for an exact match or a path under /api/events/: - if strings.HasPrefix(path, "/api/events") {
+ if path == "/api/events" || strings.HasPrefix(path, "/api/events/") {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| slog.Info("upstream not ready, sending magic packet", "req", path, "from", r.RemoteAddr) | ||||||||||||||||||||||||||||||||||
| if err := sendMagicPacket(*flagMac); err != nil { | ||||||||||||||||||||||||||||||||||
| slog.Warn("failed to send magic WoL packet", "error", err) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resource leak: cancel function must be called.
The cancel function returned by
signal.NotifyContextshould be called to release resources. Discarding it with_causes a resource leak.Apply this diff:
📝 Committable suggestion
🤖 Prompt for AI Agents