1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-10-18 00:50:11 +00:00

Compare commits

...

2 Commits

Author SHA1 Message Date
831296a3e8
remove windows related messages
self restarts now work, so no need.
2020-09-08 23:13:44 +01:00
bbb0568cc4
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.
2020-09-08 23:08:50 +01:00
5 changed files with 169 additions and 72 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"))
} }

View File

@ -269,15 +269,6 @@
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Warning</h5> <h5 class="modal-title">Warning</h5>
</div> </div>
{{ if .windows }}
<div class="modal-body">
<p>A restart is needed to apply some settings. Self-restarts aren't possible on Windows, so you must restart manually.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-secondary" id="applyRestarts" data-dismiss="modal">Apply, Restart manually</button>
</div>
{{ else }}
<div class="modal-body"> <div class="modal-body">
<p>A restart is needed to apply some settings. Restart now, later, or cancel?</p> <p>A restart is needed to apply some settings. Restart now, later, or cancel?</p>
</div> </div>
@ -286,7 +277,6 @@
<button type="button" class="btn btn-secondary" id="applyRestarts" data-dismiss="modal">Apply, Restart later</button> <button type="button" class="btn btn-secondary" id="applyRestarts" data-dismiss="modal">Apply, Restart later</button>
<button type="button" class="btn btn-primary" id="applyAndRestart" data-dismiss="modal">Apply &amp; Restart</button> <button type="button" class="btn btn-primary" id="applyAndRestart" data-dismiss="modal">Apply &amp; Restart</button>
</div> </div>
{{ end }}
</div> </div>
</div> </div>
</div> </div>

View File

@ -359,11 +359,7 @@
<div class="card-body text-center"> <div class="card-body text-center">
<h5 class="card-title">Finished!</h5> <h5 class="card-title">Finished!</h5>
<p class="card-text"> <p class="card-text">
{{ if .windows }}
Press the button below to submit your settings. Unfortunately you're running on Windows, so jfa-go <a href="https://github.com/golang/go/issues/30662">cannot restart itself.</a> You will manually have to restart.
{{ else }}
Press the button below to submit your settings. The program will restart. Once it's done, refresh this page. Press the button below to submit your settings. The program will restart. Once it's done, refresh this page.
{{ end }}
</p> </p>
<button id="submitButton" class="btn btn-primary">Submit</button> <button id="submitButton" class="btn btn-primary">Submit</button>
</div> </div>

175
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 != "" {
@ -365,37 +415,100 @@ func main() {
} }
app.info.Printf("Starting router @ %s", address) app.info.Printf("Starting router @ %s", address)
} else { } else {
windows := false
if PLATFORM == "windows" {
windows = true
}
router.GET("/", func(gc *gin.Context) { router.GET("/", func(gc *gin.Context) {
gc.HTML(200, "setup.html", gin.H{ gc.HTML(200, "setup.html", gin.H{})
"windows": windows,
})
}) })
router.POST("/testJF", app.TestJF) router.POST("/testJF", app.TestJF)
router.POST("/modifyConfig", app.ModifyConfig) router.POST("/modifyConfig", app.ModifyConfig)
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)
}
}
} }

View File

@ -11,10 +11,6 @@ func (app *appContext) AdminPage(gc *gin.Context) {
emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool() emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool()
notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool() notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool()
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false) ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false)
windows := false
if PLATFORM == "windows" {
windows = true
}
gc.HTML(http.StatusOK, "admin.html", gin.H{ gc.HTML(http.StatusOK, "admin.html", gin.H{
"bs5": bs5, "bs5": bs5,
"cssFile": app.cssFile, "cssFile": app.cssFile,
@ -24,7 +20,6 @@ func (app *appContext) AdminPage(gc *gin.Context) {
"version": VERSION, "version": VERSION,
"commit": COMMIT, "commit": COMMIT,
"ombiEnabled": ombiEnabled, "ombiEnabled": ombiEnabled,
"windows": windows,
}) })
} }