Fix service shutdown deadlock and log directory naming mismatch#8
Conversation
Co-authored-by: adcondev <38170282+adcondev@users.noreply.github.com>
Co-authored-by: adcondev <38170282+adcondev@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Fixes two service integration issues: shutdown hangs during sc stop and log folder naming not matching what the TUI installer expects.
Changes:
- Threads an ldflags-injected
ServiceNamefrommain→daemon.Service→logging.Setup()to alignProgramDatalog directories with the Windows service registry name. - Treats
http.ErrServerClosedas an expected case during shutdown rather than a fatal server error. - Updates remote environment default service name and Taskfile build flags to use the
..._Remotenaming convention.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| internal/daemon/program.go | Accepts injected service name for logging and adjusts HTTP server error handling on shutdown. |
| internal/config/config.go | Updates remote default ServiceName to match expected registry/log directory naming. |
| cmd/BasculaServicio/main.go | Adds ServiceName build-time variable and passes it into daemon.New(). |
| Taskfile.yml | Injects log-oriented service name via ldflags for local/remote builds. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if err := s.srv.ListenAndServe(); err != nil { | ||
| log.Printf("[X] Error al iniciar servidor: %v", err) | ||
| close(s.quit) | ||
| // http.ErrServerClosed is expected during graceful shutdown | ||
| if err != http.ErrServerClosed { | ||
| log.Printf("[X] Error al iniciar servidor: %v", err) | ||
| close(s.quit) | ||
| } | ||
| } | ||
| }() |
There was a problem hiding this comment.
run() previously used the ListenAndServe() return path to close s.quit. With this change, an expected graceful shutdown (http.ErrServerClosed) will no longer close s.quit, which can reintroduce the stop deadlock if the main run() loop is blocked on <-s.quit (as described in the PR context). A concrete fix is to ensure s.quit is closed on the normal stop path (e.g., in Stop() after calling server shutdown), or close s.quit when ListenAndServe() exits regardless of error type while guarding with a sync.Once to prevent double-close panics if Stop() also closes it.
There was a problem hiding this comment.
The shutdown flow is correct. Stop() calls Shutdown() which causes ListenAndServe() to return http.ErrServerClosed, then Stop() closes s.quit safely (lines 165-170) using select to prevent double-close. I've added clarifying comments to document this flow. Fixed in 455fc46.
| BuildEnvironment = "local" | ||
| BuildDate = "unknown" | ||
| BuildTime = "unknown" | ||
| ServiceName = "" | ||
| ) | ||
|
|
||
| func main() { | ||
| // Create and run service | ||
| service := daemon.New(BuildEnvironment, BuildDate, BuildTime) | ||
| service := daemon.New(BuildEnvironment, BuildDate, BuildTime, ServiceName) |
There was a problem hiding this comment.
The new ServiceName variable is described (in internal/daemon/program.go) as “used for logging” and may differ from the actual Windows service ID/display name. Consider renaming this to something more specific (e.g., LogServiceName or RegistryServiceName) to avoid confusion with the SCM service name, and keep the naming consistent with the Taskfile vars (SVC_LOG_NAME_*).
There was a problem hiding this comment.
Renamed ServiceName → LogServiceName to clarify it's used for log directory naming, not the Windows SCM service name. This aligns with the Taskfile's SVC_LOG_NAME_* variable naming. Fixed in 0ecee0c.
|
@copilot apply changes based on the comments in this thread |
Co-authored-by: adcondev <38170282+adcondev@users.noreply.github.com>
Co-authored-by: adcondev <38170282+adcondev@users.noreply.github.com>
Co-authored-by: adcondev <38170282+adcondev@users.noreply.github.com>
Fix Plan: Graceful Shutdown Deadlock & Log Directory Mismatch ✅
Issue 1: Graceful Shutdown Deadlock ✅
handler.goalready hashttpServerfield andShutdown()methodprogram.goalready callssrv.Shutdown()inStop()run()inprogram.goto handlehttp.ErrServerClosedas normal shutdown (not fatal)net/httpimport toprogram.goStop()closess.quitsafely after server shutdown completesIssue 2: Log Directory Naming Mismatch ✅
LogServiceNamevariable tocmd/BasculaServicio/main.go(renamed fromServiceName)daemon.New()to acceptlogServiceNameparameterLogServiceNameinServicestruct with comprehensive documentationLogServiceNameinInit()for logging setup instead ofs.env.ServiceNamemain.LogServiceNameCode Review Feedback ✅
ServiceName→LogServiceNameto clarify it's for logging, not Windows SCM service nameSVC_LOG_NAME_*)s.quitis closedTesting & Verification ✅
Original prompt
Context
The
poster-tuisTUI installer (https://github.com/adcondev/poster-tuis) manages the lifecycle of bothscale-daemonandticket-daemonWindows services. During testing, two critical issues were identified:sc stopis sent, leaving it inSTOP_PENDINGstate until force-killed from Task Manager. The ticket-daemon stops cleanly with the exact same TUI code.Issue 1: Graceful Shutdown Deadlock (CRITICAL)
Root Cause
The
Stop()method ininternal/daemon/program.godeadlocks because:Stop()callss.wg.Wait()which waits for therun()goroutine to finishrun()is blocked on<-s.quit— waiting for the quit channel to closeListenAndServe()returns with an error (line ~122 inrun())ListenAndServe()ininternal/server/handler.gouses the rawhttp.ListenAndServe()function which has no way to be shut down gracefully — there's no stored*http.ServerreferenceListenAndServe()never returns →<-s.quitblocks forever →run()never exits →wg.Wait()deadlocks →Stop()hangs → Windows SCM times outThe deadlock chain:
Why ticket-daemon works
The ticket-daemon's
Stop()does it correctly:*http.Serverand callshttpServer.Shutdown(ctx)— gracefully shuts down HTTP serverwsServer.Shutdown()— closes all WebSocket clientsclose(p.quit)— explicitly unblocksrun()p.wg.Wait()— now completes becauserun()can exitRequired Fix
In
internal/server/handler.go:httpServer *http.Serverfield to theServerstructListenAndServe(), create an*http.Server{Addr: ..., Handler: mux}and store it before callings.httpServer.ListenAndServe()Shutdown(ctx context.Context) errormethod that callss.httpServer.Shutdown(ctx)if non-nilhttp.ErrServerClosedgracefully inrun()(not as a fatal error — it's the normal shutdown path)In
internal/daemon/program.goStop()method:s.srv.Shutdown(ctx)with a 10-second timeout to gracefully close the HTTP serverclose(s.quit)to unblockrun()s.wg.Wait()which will now completerun()to check forhttp.ErrServerClosedand not treat it as a fatal errorIssue 2: Log Directory Naming Mismatch
What the Taskfile defines and TUI expects
The Taskfile naming convention is
R2k_<<Daemon>><<Type>>_<<Env>>. The Taskfile injectsmain.ServiceNamevia ldflags:-X main.ServiceName=R2k_BasculaServicio_Local-X main.ServiceName=R2k_BasculaServicio_RemoteThe TUI installer (
poster-tuis) constructs log paths using the Windows service registry name (RegistryName), which follows this convention:What the daemon actually does
The
ServiceNamevariable frommainpackage is injected but never forwarded to the logging setup. Instead,internal/config/config.gohas hardcodedServiceNamevalues in theEnvironmentsmap:The logging setup in
internal/logging/logging.gouses thisServiceNamefor the log directory:So the actual log files end up at:
But the TUI and the desired convention expect:
Required Fix — Make ServiceName injectable from ldflags
The
main.ServiceNamevariable (which IS correctly injected by the Taskfile asR2k_BasculaServicio_Local/R2k_BasculaServicio_Remote) needs to be forwarded through to the loggin...This pull request was created from Copilot chat.
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.