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: http-password:
database-driver: internal database-driver: internal
database-dsn: root:photoprism@tcp(localhost:4000)/photoprism?parseTime=true 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{ app.Commands = []cli.Command{
commands.ConfigCommand, commands.ConfigCommand,
commands.StartCommand, commands.StartCommand,
commands.StopCommand,
commands.MigrateCommand, commands.MigrateCommand,
commands.ImportCommand, commands.ImportCommand,
commands.IndexCommand, 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/pingcap/tidb-tools v2.1.3-0.20190116051332-34c808eef588+incompatible
github.com/pkg/errors v0.8.1 github.com/pkg/errors v0.8.1
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829 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/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446 // indirect github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446 // indirect
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd
github.com/satori/go.uuid v1.2.0 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/simplereach/timeutils v1.2.0 // indirect
github.com/sirupsen/logrus v1.2.0 github.com/sirupsen/logrus v1.2.0
github.com/soheilhy/cmux v0.1.4 // indirect 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/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 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 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 h1:btgOAlu9RW6de2r2qQiONhjgxdAG7BL6je0G6J/yPnA=
github.com/simplereach/timeutils v1.2.0/go.mod h1:VVbQDfN/FHRZa1LSqcwo4kNZ62OOyqLLGQKYB3pB0Q8= 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= github.com/sirupsen/logrus v0.0.0-20170323161349-3bcb09397d6d/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=

View file

@ -4,13 +4,15 @@ import (
"context" "context"
"os" "os"
"os/signal" "os/signal"
"strconv"
"syscall" "syscall"
"time" "time"
log "github.com/sirupsen/logrus"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/server" "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" "github.com/urfave/cli"
) )
@ -41,6 +43,12 @@ var startFlags = []cli.Flag{
Value: "", Value: "",
EnvVar: "PHOTOPRISM_HTTP_MODE", EnvVar: "PHOTOPRISM_HTTP_MODE",
}, },
cli.BoolFlag{
Name: "daemonize, d",
Usage: "run Photoprism as Daemon",
EnvVar: "PHOTOPRISM_DAEMON_MODE",
},
} }
func startAction(ctx *cli.Context) error { func startAction(ctx *cli.Context) error {
@ -60,6 +68,33 @@ func startAction(ctx *cli.Context) error {
} }
conf.MigrateDb() 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()) log.Infof("starting web server at %s:%d", conf.HttpServerHost(), conf.HttpServerPort())
go server.Start(cctx, conf) go server.Start(cctx, conf)
@ -70,6 +105,28 @@ func startAction(ctx *cli.Context) error {
log.Info("Shutting down...") log.Info("Shutting down...")
conf.Shutdown() conf.Shutdown()
cancel() cancel()
err := dctx.Release()
if err != nil {
log.Error(err)
}
time.Sleep(3 * time.Second) time.Sleep(3 * time.Second)
return nil 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 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). // SqlServerHost returns the built-in SQL server host name or IP address (empty for all interfaces).
func (c *Config) SqlServerHost() string { func (c *Config) SqlServerHost() string {
return c.config.SqlServerHost return c.config.SqlServerHost

View file

@ -1,6 +1,8 @@
package config package config
import "github.com/urfave/cli" import (
"github.com/urfave/cli"
)
// Global CLI flags // Global CLI flags
var GlobalFlags = []cli.Flag{ var GlobalFlags = []cli.Flag{
@ -150,4 +152,21 @@ var GlobalFlags = []cli.Flag{
Value: "heif-convert", Value: "heif-convert",
EnvVar: "PHOTOPRISM_HEIFCONVERT_BIN", 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"` DarktableBin string `yaml:"darktable-bin" flag:"darktable-bin"`
ExifToolBin string `yaml:"exiftool-bin" flag:"exiftool-bin"` ExifToolBin string `yaml:"exiftool-bin" flag:"exiftool-bin"`
HeifConvertBin string `yaml:"heifconvert-bin" flag:"heifconvert-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: // 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 { if len(openstreetmapLocation.Name) > 1 {
location.LocName = strings.ReplaceAll(openstreetmapLocation.Name, " - ", " / ") location.LocName = strings.Replace(openstreetmapLocation.Name, " - ", " / ", -1)
location.LocName = util.Title(strings.TrimSpace(strings.ReplaceAll(location.LocName, "_", " "))) location.LocName = util.Title(strings.TrimSpace(strings.Replace(location.LocName, "_", " ", -1)))
} }
location.LocHouseNr = strings.TrimSpace(openstreetmapLocation.Address.HouseNumber) 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.LocCountryCode = strings.TrimSpace(openstreetmapLocation.Address.CountryCode)
location.LocDisplayName = strings.TrimSpace(openstreetmapLocation.DisplayName) location.LocDisplayName = strings.TrimSpace(openstreetmapLocation.DisplayName)
locationCategory := strings.TrimSpace(strings.ReplaceAll(openstreetmapLocation.Category, "_", " ")) locationCategory := strings.TrimSpace(strings.Replace(openstreetmapLocation.Category, "_", " ", -1))
location.LocCategory = locationCategory location.LocCategory = locationCategory
if openstreetmapLocation.Type != "yes" && openstreetmapLocation.Type != "unclassified" { 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 location.LocType = locationType
} }

View file

@ -18,6 +18,17 @@ func Exists(filename string) bool {
return err == nil && !info.IsDir() 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 // Returns full path; ~ replaced with actual home directory
func ExpandedFilename(filename string) string { func ExpandedFilename(filename string) string {
if filename == "" { if filename == "" {