pat/main.go
Martin Hebnes Pedersen 9bc6ac22b1 Refactor reload handling
2025-06-22 20:22:37 +02:00

154 lines
4.4 KiB
Go

// Copyright 2016 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.
// A portable Winlink client for amateur radio email.
package main
import (
"context"
"embed"
"fmt"
"log"
"os"
"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"
"github.com/spf13/pflag"
)
//go:embed web/dist/**
var embeddedFS embed.FS
func init() {
api.EmbeddedFS = embeddedFS
pflag.Usage = func() {
fmt.Fprintf(os.Stderr, "%s is a client for the Winlink 2000 Network.\n\n", buildinfo.AppName)
fmt.Fprintf(os.Stderr, "Usage:\n %s [options] command [arguments]\n", os.Args[0])
fmt.Fprintln(os.Stderr, "\nCommands:")
for _, cmd := range cli.Commands {
fmt.Fprintf(os.Stderr, " %-15s %s\n", cmd.Str, cmd.Desc)
}
fmt.Fprintln(os.Stderr, "\nOptions:")
optionsSet(&app.Options{}).PrintDefaults()
fmt.Fprint(os.Stderr, "\n")
}
}
func main() {
cmd, options, args, err := cli.FindCommand(os.Args)
if err != nil {
pflag.Usage()
os.Exit(1)
}
var opts app.Options
optionsSet(&opts).Parse(options)
if len(args) == 0 {
args = append(args, "")
}
switch args[0] {
case "--help", "-help", "help", "-h":
cmd.PrintUsage()
os.Exit(1)
}
if cmd.Str == "help" {
cli.HelpHandle(args)
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/reload handling.
shouldReload := make(chan bool, 1)
done := make(chan struct{})
a.OnReload = func() error {
// Avoid reloading of bad config
if _, err := app.LoadConfig(opts.ConfigPath, cfg.DefaultConfig); err != nil {
return fmt.Errorf("bad config: %v", err)
}
cancel()
shouldReload <- true
return nil
}
go func() {
defer close(shouldReload)
dirtyDisconnectNext := false // So we can do a dirty disconnect on the second interrupt
for {
select {
case s := <-sig:
switch {
case isSIGHUP(s):
if err := a.Reload(); err != nil {
log.Printf("Ignoring live reload due to error: %v", err)
continue
}
return
default:
if ok := a.AbortActiveConnection(dirtyDisconnectNext); ok {
dirtyDisconnectNext = !dirtyDisconnectNext
continue
}
cancel()
shouldReload <- false
return
}
case <-done:
return
}
}
}()
// Run the app
a.Run(ctx, cmd, args)
close(done)
return <-shouldReload
}
func optionsSet(opts *app.Options) *pflag.FlagSet {
set := pflag.NewFlagSet("options", pflag.ExitOnError)
set.StringVar(&opts.MyCall, "mycall", "", "Your callsign (winlink user).")
set.StringVarP(&opts.Listen, "listen", "l", "", "Comma-separated list of methods to listen on (e.g. ardop,telnet,ax25).")
set.BoolVarP(&opts.SendOnly, "send-only", "s", false, "Download inbound messages later, send only.")
set.BoolVarP(&opts.RadioOnly, "radio-only", "", false, "Radio Only mode (Winlink Hybrid RMS only).")
set.BoolVar(&opts.IgnoreBusy, "ignore-busy", false, "Don't wait for clear channel before connecting to a node.")
defaultMBox := filepath.Join(directories.DataDir(), "mailbox")
defaultFormsPath := filepath.Join(directories.DataDir(), "Standard_Forms")
defaultConfigPath := filepath.Join(directories.ConfigDir(), "config.json")
defaultPrehooksPath := filepath.Join(directories.ConfigDir(), "prehooks")
defaultLogPath := filepath.Join(directories.StateDir(), strings.ToLower(buildinfo.AppName+".log"))
defaultEventLogPath := filepath.Join(directories.StateDir(), "eventlog.json")
set.StringVar(&opts.MailboxPath, "mbox", defaultMBox, "Path to mailbox directory.")
set.StringVar(&opts.FormsPath, "forms", defaultFormsPath, "Path to forms directory.")
set.StringVar(&opts.ConfigPath, "config", defaultConfigPath, "Path to config file.")
set.StringVar(&opts.PrehooksPath, "prehooks", defaultPrehooksPath, "Path to prehooks")
set.StringVar(&opts.LogPath, "log", defaultLogPath, "Path to log file. The file is truncated on each startup.")
set.StringVar(&opts.EventLogPath, "event-log", defaultEventLogPath, "Path to event log file.")
return set
}