1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-12-22 17:10:10 +00:00

basic daemon functionality, self-restarts without syscall.exec

running 'jfa-go start' will run it as a daemon in the background, and
'jfa-go stop' will tell it to quit via a unix socket. Self-restarts are
now implented by simply exiting the main function (now called start) and
running it again.
This commit is contained in:
Harvey Tindall 2020-09-08 23:08:50 +01:00
parent f5f2a0f190
commit bbb0568cc4
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2
2 changed files with 168 additions and 46 deletions

47
api.go
View File

@ -2,10 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"os"
"os/signal"
"strings" "strings"
"syscall"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -699,24 +696,30 @@ func (app *appContext) Logout(gc *gin.Context) {
// panic(fmt.Errorf("restarting")) // panic(fmt.Errorf("restarting"))
// } // }
// func (app *appContext) Restart() error {
// defer func() {
// if r := recover(); r != nil {
// signal.Notify(app.quit, os.Interrupt)
// <-app.quit
// }
// }()
// args := os.Args
// // After a single restart, args[0] gets messed up and isnt the real executable.
// // JFA_DEEP tells the new process its a child, and JFA_EXEC is the real executable
// if os.Getenv("JFA_DEEP") == "" {
// os.Setenv("JFA_DEEP", "1")
// os.Setenv("JFA_EXEC", args[0])
// }
// env := os.Environ()
// err := syscall.Exec(os.Getenv("JFA_EXEC"), []string{""}, env)
// if err != nil {
// return err
// }
// panic(fmt.Errorf("restarting"))
// }
// no need to syscall.exec anymore!
func (app *appContext) Restart() error { func (app *appContext) Restart() error {
defer func() { RESTART <- true
if r := recover(); r != nil { return nil
signal.Notify(app.quit, os.Interrupt)
<-app.quit
}
}()
args := os.Args
// After a single restart, args[0] gets messed up and isnt the real executable.
// JFA_DEEP tells the new process its a child, and JFA_EXEC is the real executable
if os.Getenv("JFA_DEEP") == "" {
os.Setenv("JFA_DEEP", "1")
os.Setenv("JFA_EXEC", args[0])
}
env := os.Environ()
err := syscall.Exec(os.Getenv("JFA_EXEC"), []string{""}, env)
if err != nil {
return err
}
panic(fmt.Errorf("restarting"))
} }

165
main.go
View File

@ -10,8 +10,10 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
"net"
"net/http" "net/http"
"os" "os"
"os/exec"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"runtime" "runtime"
@ -96,10 +98,17 @@ func setGinLogger(router *gin.Engine, debugMode bool) {
} }
} }
var PLATFORM string = runtime.GOOS var (
PLATFORM string = runtime.GOOS
SOCK string = "jfa-go.sock"
SRV *http.Server
RESTART chan bool
DATA, CONFIG, HOST *string
PORT *int
DEBUG *bool
)
func main() { func start(asDaemon, firstCall bool) {
fmt.Printf("jfa-go version: %s (%s)\n", VERSION, COMMIT)
// app encompasses essentially all useful functions. // app encompasses essentially all useful functions.
app := new(appContext) app := new(appContext)
@ -117,23 +126,25 @@ func main() {
app.info = log.New(os.Stdout, "[INFO] ", log.Ltime) app.info = log.New(os.Stdout, "[INFO] ", log.Ltime)
app.err = log.New(os.Stdout, "[ERROR] ", log.Ltime|log.Lshortfile) app.err = log.New(os.Stdout, "[ERROR] ", log.Ltime|log.Lshortfile)
dataPath := flag.String("data", app.data_path, "alternate path to data directory.") if firstCall {
configPath := flag.String("config", app.config_path, "alternate path to config file.") DATA = flag.String("data", app.data_path, "alternate path to data directory.")
host := flag.String("host", "", "alternate address to host web ui on.") CONFIG = flag.String("config", app.config_path, "alternate path to config file.")
port := flag.Int("port", 0, "alternate port to host web ui on.") HOST = flag.String("host", "", "alternate address to host web ui on.")
debug := flag.Bool("debug", false, "Enables debug logging and exposes pprof.") PORT = flag.Int("port", 0, "alternate port to host web ui on.")
DEBUG = flag.Bool("debug", false, "Enables debug logging and exposes pprof.")
flag.Parse() flag.Parse()
}
// attempt to apply command line flags correctly // attempt to apply command line flags correctly
if app.config_path == *configPath && app.data_path != *dataPath { if app.config_path == *CONFIG && app.data_path != *DATA {
app.data_path = *dataPath app.data_path = *DATA
app.config_path = filepath.Join(app.data_path, "config.ini") app.config_path = filepath.Join(app.data_path, "config.ini")
} else if app.config_path != *configPath && app.data_path == *dataPath { } else if app.config_path != *CONFIG && app.data_path == *DATA {
app.config_path = *configPath app.config_path = *CONFIG
} else { } else {
app.config_path = *configPath app.config_path = *CONFIG
app.data_path = *dataPath app.data_path = *DATA
} }
// env variables are necessary because syscall.Exec for self-restarts doesn't doesn't work with arguments for some reason. // env variables are necessary because syscall.Exec for self-restarts doesn't doesn't work with arguments for some reason.
@ -183,7 +194,7 @@ func main() {
// read from config... // read from config...
debugMode = app.config.Section("ui").Key("debug").MustBool(false) debugMode = app.config.Section("ui").Key("debug").MustBool(false)
// then from flag // then from flag
if *debug { if *DEBUG {
debugMode = true debugMode = true
} }
if debugMode { if debugMode {
@ -193,15 +204,54 @@ func main() {
app.debug = log.New(ioutil.Discard, "", 0) app.debug = log.New(ioutil.Discard, "", 0)
} }
if asDaemon {
go func() {
socket := SOCK
os.Remove(socket)
listener, err := net.Listen("unix", socket)
if err != nil {
app.err.Fatalf("Couldn't establish socket connection at %s\n", SOCK)
}
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
<-c
os.Remove(socket)
os.Exit(1)
}()
defer func() {
listener.Close()
os.Remove(SOCK)
}()
for {
con, err := listener.Accept()
if err != nil {
app.err.Printf("Couldn't read message on %s: %s", socket, err)
continue
}
buf := make([]byte, 512)
nr, err := con.Read(buf)
if err != nil {
app.err.Printf("Couldn't read message on %s: %s", socket, err)
continue
}
command := string(buf[0:nr])
if command == "stop" {
app.shutdown()
}
}
}()
}
if !firstRun { if !firstRun {
app.host = app.config.Section("ui").Key("host").String() app.host = app.config.Section("ui").Key("host").String()
app.port = app.config.Section("ui").Key("port").MustInt(8056) app.port = app.config.Section("ui").Key("port").MustInt(8056)
if *host != app.host && *host != "" { if *HOST != app.host && *HOST != "" {
app.host = *host app.host = *HOST
} }
if *port != app.port && *port > 0 { if *PORT != app.port && *PORT > 0 {
app.port = *port app.port = *PORT
} }
if h := os.Getenv("JFA_HOST"); h != "" { if h := os.Getenv("JFA_HOST"); h != "" {
@ -379,23 +429,92 @@ func main() {
app.info.Printf("Loading setup @ %s", address) app.info.Printf("Loading setup @ %s", address)
} }
srv := &http.Server{ SRV = &http.Server{
Addr: address, Addr: address,
Handler: router, Handler: router,
} }
go func() { go func() {
if err := srv.ListenAndServe(); err != nil { if err := SRV.ListenAndServe(); err != nil {
app.err.Printf("Failure serving: %s", err) app.err.Printf("Failure serving: %s", err)
} }
}() }()
app.quit = make(chan os.Signal) app.quit = make(chan os.Signal)
signal.Notify(app.quit, os.Interrupt) signal.Notify(app.quit, os.Interrupt)
<-app.quit go func() {
for range app.quit {
app.shutdown()
}
}()
for range RESTART {
cntx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
if err := SRV.Shutdown(cntx); err != nil {
app.err.Fatalf("Server shutdown error: %s", err)
}
return
}
}
func (app *appContext) shutdown() {
app.info.Println("Shutting down...") app.info.Println("Shutting down...")
cntx, cancel := context.WithTimeout(context.Background(), time.Second*5) cntx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel() defer cancel()
if err := srv.Shutdown(cntx); err != nil { if err := SRV.Shutdown(cntx); err != nil {
app.err.Fatalf("Server shutdown error: %s", err) app.err.Fatalf("Server shutdown error: %s", err)
} }
os.Exit(1)
}
func flagPassed(name string) (found bool) {
for _, f := range os.Args {
if f == name {
found = true
}
}
return
}
func main() {
fmt.Printf("jfa-go version: %s (%s)\n", VERSION, COMMIT)
folder := "/tmp"
if PLATFORM == "windows" {
folder = os.Getenv("TEMP")
}
SOCK = filepath.Join(folder, SOCK)
fmt.Println(SOCK)
if flagPassed("start") {
args := []string{}
for i, f := range os.Args {
if f == "start" {
args = append(args, "daemon")
} else if i != 0 {
args = append(args, f)
}
}
cmd := exec.Command(os.Args[0], args...)
cmd.Start()
os.Exit(1)
} else if flagPassed("stop") {
con, err := net.Dial("unix", SOCK)
if err != nil {
fmt.Printf("Couldn't dial socket %s, are you sure jfa-go is running?\n", SOCK)
os.Exit(1)
}
_, err = con.Write([]byte("stop"))
if err != nil {
fmt.Printf("Couldn't send command to socket %s, are you sure jfa-go is running?\n", SOCK)
os.Exit(1)
}
fmt.Println("Sent.")
} else if flagPassed("daemon") {
start(true, true)
} else {
RESTART = make(chan bool, 1)
start(false, true)
for {
fmt.Printf("jfa-go version: %s (%s)\n", VERSION, COMMIT)
start(false, false)
}
}
} }