Implement daemon mode (#119)

* add daemon process
* add daemon stop command
* add daemon log to log file
* check for running daemons
* minor changes
This commit is contained in:
Vedhavyas Singareddi 2019-06-20 01:11:03 +02:00 committed by Michael Mayer
parent 42d344b4b5
commit f63efc5f51
11 changed files with 170 additions and 7 deletions

View file

@ -15,3 +15,6 @@ http-port: 80
http-password:
database-driver: internal
database-dsn: root:photoprism@tcp(localhost:4000)/photoprism?parseTime=true
daemon-pid-path: /srv/photoprism/photoprism.pid
daemon-log-path: /srv/photoprism/photoprism.log
daemon-mode: false

View file

@ -23,6 +23,7 @@ func main() {
app.Commands = []cli.Command{
commands.ConfigCommand,
commands.StartCommand,
commands.StopCommand,
commands.MigrateCommand,
commands.ImportCommand,
commands.IndexCommand,

2
go.mod
View file

@ -36,10 +36,12 @@ require (
github.com/pingcap/tidb-tools v2.1.3-0.20190116051332-34c808eef588+incompatible
github.com/pkg/errors v0.8.1
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829
github.com/prometheus/common v0.2.0
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446 // indirect
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd
github.com/satori/go.uuid v1.2.0
github.com/sevlyar/go-daemon v0.1.5
github.com/simplereach/timeutils v1.2.0 // indirect
github.com/sirupsen/logrus v1.2.0
github.com/soheilhy/cmux v0.1.4 // indirect

2
go.sum
View file

@ -257,6 +257,8 @@ github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0
github.com/rwcarlsen/goexif v0.0.0-20190501182100-9e8deecbddbd4989a3e8d003684b783412b41e7a/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sevlyar/go-daemon v0.1.5 h1:Zy/6jLbM8CfqJ4x4RPr7MJlSKt90f00kNM1D401C+Qk=
github.com/sevlyar/go-daemon v0.1.5/go.mod h1:6dJpPatBT9eUwM5VCw9Bt6CdX9Tk6UWvhW3MebLDRKE=
github.com/simplereach/timeutils v1.2.0 h1:btgOAlu9RW6de2r2qQiONhjgxdAG7BL6je0G6J/yPnA=
github.com/simplereach/timeutils v1.2.0/go.mod h1:VVbQDfN/FHRZa1LSqcwo4kNZ62OOyqLLGQKYB3pB0Q8=
github.com/sirupsen/logrus v0.0.0-20170323161349-3bcb09397d6d/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=

View file

@ -4,13 +4,15 @@ import (
"context"
"os"
"os/signal"
"strconv"
"syscall"
"time"
log "github.com/sirupsen/logrus"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/server"
"github.com/photoprism/photoprism/internal/util"
daemon "github.com/sevlyar/go-daemon"
log "github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -41,6 +43,12 @@ var startFlags = []cli.Flag{
Value: "",
EnvVar: "PHOTOPRISM_HTTP_MODE",
},
cli.BoolFlag{
Name: "daemonize, d",
Usage: "run Photoprism as Daemon",
EnvVar: "PHOTOPRISM_DAEMON_MODE",
},
}
func startAction(ctx *cli.Context) error {
@ -60,6 +68,33 @@ func startAction(ctx *cli.Context) error {
}
conf.MigrateDb()
dctx := new(daemon.Context)
dctx.LogFileName = conf.DaemonLogPath()
dctx.PidFileName = conf.DaemonPIDPath()
dctx.Args = ctx.Args()
if !daemon.WasReborn() && conf.ShouldDaemonize() {
conf.Shutdown()
cancel()
if pid, ok := childAlreadyRunning(conf.DaemonPIDPath()); ok {
log.Infof("Daemon already running with PID[%v]\n", pid)
return nil
}
child, err := dctx.Reborn()
if err != nil {
log.Fatal(err)
}
if child != nil {
if !util.Overwrite(conf.DaemonPIDPath(), []byte(strconv.Itoa(child.Pid))) {
log.Fatal("failed to write PID to file")
}
log.Infof("Daemon started with PID: %v\n", child.Pid)
return nil
}
}
log.Infof("starting web server at %s:%d", conf.HttpServerHost(), conf.HttpServerPort())
go server.Start(cctx, conf)
@ -70,6 +105,28 @@ func startAction(ctx *cli.Context) error {
log.Info("Shutting down...")
conf.Shutdown()
cancel()
err := dctx.Release()
if err != nil {
log.Error(err)
}
time.Sleep(3 * time.Second)
return nil
}
func childAlreadyRunning(filePath string) (pid int, running bool) {
if !util.Exists(filePath) {
return pid, false
}
pid, err := daemon.ReadPidFile(filePath)
if err != nil {
return pid, false
}
process, err := os.FindProcess(int(pid))
if err != nil {
return pid, false
}
return pid, process.Signal(syscall.Signal(0)) == nil
}

42
internal/commands/stop.go Normal file
View file

@ -0,0 +1,42 @@
package commands
import (
"syscall"
"github.com/photoprism/photoprism/internal/config"
"github.com/prometheus/common/log"
daemon "github.com/sevlyar/go-daemon"
"github.com/urfave/cli"
)
// StopCommand stops the daemon if any.
var StopCommand = cli.Command{
Name: "stop",
Usage: "Stops daemon",
Action: stopAction,
}
func stopAction(ctx *cli.Context) error {
conf := config.NewConfig(ctx)
log.Infof("Looking for PID from file: %v\n", conf.DaemonPIDPath())
dcxt := new(daemon.Context)
dcxt.PidFileName = conf.DaemonPIDPath()
child, err := dcxt.Search()
if err != nil {
log.Fatal(err)
}
err = child.Signal(syscall.SIGTERM)
if err != nil {
log.Fatal(err)
}
st, err := child.Wait()
if err != nil {
log.Info("Daemon exited successfully")
return nil
}
log.Infof("Daemon[%v] exited[%v]? successfully[%v]?\n", st.Pid(), st.Exited(), st.Success())
return nil
}

View file

@ -218,6 +218,29 @@ func (c *Config) ConfigPath() string {
return c.config.ConfigPath
}
// DaemonPIDPath returns the filepath of the pid.
func (c *Config) DaemonPIDPath() string {
if c.config.DaemonPIDPath == "" {
return c.AssetsPath() + "/photoprism.pid"
}
return c.config.DaemonPIDPath
}
// DaemonLogPath returns the filepath of the log.
func (c *Config) DaemonLogPath() string {
if c.config.DaemonLogPath == "" {
return c.AssetsPath() + "/photoprism.log"
}
return c.config.DaemonLogPath
}
// ShouldDaemonize returns true if daemon mode is set to true.
func (c *Config) ShouldDaemonize() bool {
return c.config.DaemonMode
}
// SqlServerHost returns the built-in SQL server host name or IP address (empty for all interfaces).
func (c *Config) SqlServerHost() string {
return c.config.SqlServerHost

View file

@ -1,6 +1,8 @@
package config
import "github.com/urfave/cli"
import (
"github.com/urfave/cli"
)
// Global CLI flags
var GlobalFlags = []cli.Flag{
@ -150,4 +152,21 @@ var GlobalFlags = []cli.Flag{
Value: "heif-convert",
EnvVar: "PHOTOPRISM_HEIFCONVERT_BIN",
},
cli.StringFlag{
Name: "daemon-pid-path",
Usage: "File path to store daemon PID",
EnvVar: "PHOTOPRISM_DAEMON_PID_PATH",
Value: "/srv/photoprism/photoprism.pid",
},
cli.StringFlag{
Name: "daemon-log-path",
Usage: "File path for daemon logs.",
EnvVar: "PHOTOPRISM_DAEMON_LOG_PATH",
Value: "/srv/photoprism/photoprism.log",
},
cli.BoolFlag{
Name: "daemonize, d",
Usage: "run Photoprism as Daemon",
EnvVar: "PHOTOPRISM_DAEMON_MODE",
},
}

View file

@ -56,6 +56,9 @@ type Params struct {
DarktableBin string `yaml:"darktable-bin" flag:"darktable-bin"`
ExifToolBin string `yaml:"exiftool-bin" flag:"exiftool-bin"`
HeifConvertBin string `yaml:"heifconvert-bin" flag:"heifconvert-bin"`
DaemonPIDPath string `yaml:"daemon-pid-path" flag:"daemon-pid-path"`
DaemonLogPath string `yaml:"daemon-log-path" flag:"daemon-log-path"`
DaemonMode bool `yaml:"daemon-mode" flag:"daemonize"`
}
// NewParams() creates a new configuration entity by using two methods:

View file

@ -92,8 +92,8 @@ func (m *MediaFile) Location() (*models.Location, error) {
}
if len(openstreetmapLocation.Name) > 1 {
location.LocName = strings.ReplaceAll(openstreetmapLocation.Name, " - ", " / ")
location.LocName = util.Title(strings.TrimSpace(strings.ReplaceAll(location.LocName, "_", " ")))
location.LocName = strings.Replace(openstreetmapLocation.Name, " - ", " / ", -1)
location.LocName = util.Title(strings.TrimSpace(strings.Replace(location.LocName, "_", " ", -1)))
}
location.LocHouseNr = strings.TrimSpace(openstreetmapLocation.Address.HouseNumber)
@ -106,11 +106,11 @@ func (m *MediaFile) Location() (*models.Location, error) {
location.LocCountryCode = strings.TrimSpace(openstreetmapLocation.Address.CountryCode)
location.LocDisplayName = strings.TrimSpace(openstreetmapLocation.DisplayName)
locationCategory := strings.TrimSpace(strings.ReplaceAll(openstreetmapLocation.Category, "_", " "))
locationCategory := strings.TrimSpace(strings.Replace(openstreetmapLocation.Category, "_", " ", -1))
location.LocCategory = locationCategory
if openstreetmapLocation.Type != "yes" && openstreetmapLocation.Type != "unclassified" {
locationType := strings.TrimSpace(strings.ReplaceAll(openstreetmapLocation.Type, "_", " "))
locationType := strings.TrimSpace(strings.Replace(openstreetmapLocation.Type, "_", " ", -1))
location.LocType = locationType
}

View file

@ -18,6 +18,17 @@ func Exists(filename string) bool {
return err == nil && !info.IsDir()
}
// Overwrite overwrites the file with data. Creates file if not present.
func Overwrite(fileName string, data []byte) bool {
f, err := os.Create(fileName)
if err != nil {
return false
}
_, err = f.Write(data)
return err == nil
}
// Returns full path; ~ replaced with actual home directory
func ExpandedFilename(filename string) string {
if filename == "" {