Add SIGHUP-based live reload functionality

Ref issue #6
This commit is contained in:
Martin Hebnes Pedersen 2025-06-22 13:10:12 +02:00
parent 6fe9a4f403
commit addbefa74a
3 changed files with 83 additions and 14 deletions

58
main.go
View file

@ -9,13 +9,14 @@ import (
"context"
"embed"
"fmt"
"log"
"os"
"os/signal"
"path/filepath"
"strings"
"github.com/la5nta/pat/api"
"github.com/la5nta/pat/app"
"github.com/la5nta/pat/cfg"
"github.com/la5nta/pat/cli"
"github.com/la5nta/pat/internal/buildinfo"
"github.com/la5nta/pat/internal/directories"
@ -66,30 +67,59 @@ func main() {
return
}
sig := notifySignals()
// Run app in a loop for config reloading
for runApp(opts, cmd, args, sig) {
fmt.Fprintln(os.Stderr, "Reloading application...")
}
}
func runApp(opts app.Options, cmd app.Command, args []string, sig <-chan os.Signal) bool {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
a := app.New(opts)
defer a.Close()
// Graceful shutdown by cancelling background context on interrupt.
//
// If we have an active connection, cancel that instead.
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Graceful shutdown/reload handling.
shouldReload := make(chan bool, 1)
done := make(chan struct{})
go func() {
defer close(shouldReload)
dirtyDisconnectNext := false // So we can do a dirty disconnect on the second interrupt
for range sig {
if ok := a.AbortActiveConnection(dirtyDisconnectNext); ok {
dirtyDisconnectNext = !dirtyDisconnectNext
continue
for {
select {
case s := <-sig:
switch {
case isSIGHUP(s):
// Avoid reloading of bad config
if _, err := app.LoadConfig(opts.ConfigPath, cfg.DefaultConfig); err != nil {
log.Printf("Ignoring live reload due to config error: %v", err)
continue
}
cancel()
shouldReload <- true
return
default:
if ok := a.AbortActiveConnection(dirtyDisconnectNext); ok {
dirtyDisconnectNext = !dirtyDisconnectNext
continue
}
cancel()
shouldReload <- false
return
}
case <-done:
return
}
cancel()
return
}
}()
// Run the app
a.Run(ctx, cmd, args)
close(done)
return <-shouldReload
}
func optionsSet(opts *app.Options) *pflag.FlagSet {

21
sighup_unix.go Normal file
View file

@ -0,0 +1,21 @@
// Copyright 2022 Martin Hebnes Pedersen (LA5NTA). All rights reserved.
// Use of this source code is governed by the MIT-license that can be
// found in the LICENSE file.
//go:build !windows
package main
import (
"os"
"os/signal"
"syscall"
)
func notifySignals() <-chan os.Signal {
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGHUP)
return sig
}
func isSIGHUP(s os.Signal) bool { return s == syscall.SIGHUP }

18
sighup_windows.go Normal file
View file

@ -0,0 +1,18 @@
// Copyright 2022 Martin Hebnes Pedersen (LA5NTA). All rights reserved.
// Use of this source code is governed by the MIT-license that can be
// found in the LICENSE file.
//go:build windows
package main
import (
"os"
"os/signal"
)
func notifySignals(sig chan<- os.Signal) {
signal.Notify(sig, os.Interrupt)
}
func isSIGHUP(s os.Signal) bool { return false }