From f63efc5f51586807dbc5d5ab359e7edfe1bed981 Mon Sep 17 00:00:00 2001 From: Vedhavyas Singareddi Date: Thu, 20 Jun 2019 01:11:03 +0200 Subject: [PATCH] Implement daemon mode (#119) * add daemon process * add daemon stop command * add daemon log to log file * check for running daemons * minor changes --- assets/config/photoprism.yml | 3 ++ cmd/photoprism/photoprism.go | 1 + go.mod | 2 + go.sum | 2 + internal/commands/start.go | 61 +++++++++++++++++++++++++++- internal/commands/stop.go | 42 +++++++++++++++++++ internal/config/config.go | 23 +++++++++++ internal/config/flags.go | 21 +++++++++- internal/config/params.go | 3 ++ internal/photoprism/openstreetmap.go | 8 ++-- internal/util/file.go | 11 +++++ 11 files changed, 170 insertions(+), 7 deletions(-) create mode 100644 internal/commands/stop.go diff --git a/assets/config/photoprism.yml b/assets/config/photoprism.yml index 1af83425b..ccc7fccab 100644 --- a/assets/config/photoprism.yml +++ b/assets/config/photoprism.yml @@ -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 diff --git a/cmd/photoprism/photoprism.go b/cmd/photoprism/photoprism.go index 5144b49d1..4f6c6d262 100644 --- a/cmd/photoprism/photoprism.go +++ b/cmd/photoprism/photoprism.go @@ -23,6 +23,7 @@ func main() { app.Commands = []cli.Command{ commands.ConfigCommand, commands.StartCommand, + commands.StopCommand, commands.MigrateCommand, commands.ImportCommand, commands.IndexCommand, diff --git a/go.mod b/go.mod index b80347aea..aaf35387e 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 115fd6835..866bfed1f 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/commands/start.go b/internal/commands/start.go index 117c44cf3..c4b9c204b 100644 --- a/internal/commands/start.go +++ b/internal/commands/start.go @@ -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 +} diff --git a/internal/commands/stop.go b/internal/commands/stop.go new file mode 100644 index 000000000..1f12595d1 --- /dev/null +++ b/internal/commands/stop.go @@ -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 +} diff --git a/internal/config/config.go b/internal/config/config.go index c8c8db4eb..3bc768da7 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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 diff --git a/internal/config/flags.go b/internal/config/flags.go index 641a0ac8b..72d1dd3c6 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -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", + }, } diff --git a/internal/config/params.go b/internal/config/params.go index 7f3399ecb..dc1e85c7b 100644 --- a/internal/config/params.go +++ b/internal/config/params.go @@ -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: diff --git a/internal/photoprism/openstreetmap.go b/internal/photoprism/openstreetmap.go index cc0cfd1a2..5b557c614 100644 --- a/internal/photoprism/openstreetmap.go +++ b/internal/photoprism/openstreetmap.go @@ -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 } diff --git a/internal/util/file.go b/internal/util/file.go index 5b3f1e9aa..a503759c2 100644 --- a/internal/util/file.go +++ b/internal/util/file.go @@ -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 == "" {