From e6f67417885bc033e39e2816daeb92669e80373b Mon Sep 17 00:00:00 2001 From: stunndard Date: Fri, 10 Jun 2016 15:11:14 +0800 Subject: [PATCH] daemon mode added for Linux --- config/config.go | 5 +- daemon/command_linux.go | 99 ++++++++++++++ daemon/daemon_linux.go | 266 ++++++++++++++++++++++++++++++++++++++ daemon/daemon_windows.go | 70 ++++++++++ daemon/lock_file_linux.go | 134 +++++++++++++++++++ daemon/signal_linux.go | 59 +++++++++ goicy.go | 32 ++++- goicy.ini | 9 ++ 8 files changed, 672 insertions(+), 2 deletions(-) create mode 100644 daemon/command_linux.go create mode 100644 daemon/daemon_linux.go create mode 100644 daemon/daemon_windows.go create mode 100644 daemon/lock_file_linux.go create mode 100644 daemon/signal_linux.go diff --git a/config/config.go b/config/config.go index 798665d..a5fc277 100644 --- a/config/config.go +++ b/config/config.go @@ -32,10 +32,11 @@ type Config struct { StreamGenre string `ini:"genre"` StreamPublic bool `ini:"public"` IsDaemon bool `ini:"daemon"` + PidFile string FFMPEGPath string } -const Version = "0.1" +const Version = "0.2" var Cfg Config @@ -78,6 +79,8 @@ func LoadConfig(filename string) error { Cfg.NpFile = ini.Section("misc").Key("npfile").Value() Cfg.LogFile = ini.Section("misc").Key("logfile").Value() Cfg.LogLevel, _ = ini.Section("misc").Key("loglevel").Int() + Cfg.IsDaemon, _ = ini.Section("misc").Key("daemon").Bool() + Cfg.PidFile = ini.Section("misc").Key("pidfile").Value() return nil } diff --git a/daemon/command_linux.go b/daemon/command_linux.go new file mode 100644 index 0000000..07d23c8 --- /dev/null +++ b/daemon/command_linux.go @@ -0,0 +1,99 @@ +package daemon + +import ( + "os" +) + +// AddCommand is wrapper on AddFlag and SetSigHandler functions. +func AddCommand(f Flag, sig os.Signal, handler SignalHandlerFunc) { + if f != nil { + AddFlag(f, sig) + } + if handler != nil { + SetSigHandler(handler, sig) + } +} + +// Flag is the interface implemented by an object that has two state: +// 'set' and 'unset'. +type Flag interface { + IsSet() bool +} + +// BoolFlag returns new object that implements interface Flag and +// has state 'set' when var with the given address is true. +func BoolFlag(f *bool) Flag { + return &boolFlag{f} +} + +// StringFlag returns new object that implements interface Flag and +// has state 'set' when var with the given address equals given value of v. +func StringFlag(f *string, v string) Flag { + return &stringFlag{f, v} +} + +type boolFlag struct { + b *bool +} + +func (f *boolFlag) IsSet() bool { + if f == nil { + return false + } + return *f.b +} + +type stringFlag struct { + s *string + v string +} + +func (f *stringFlag) IsSet() bool { + if f == nil { + return false + } + return *f.s == f.v +} + +var flags = make(map[Flag]os.Signal) + +// Flags returns flags that was added by the function AddFlag. +func Flags() map[Flag]os.Signal { + return flags +} + +// AddFlag adds the flag and signal to the internal map. +func AddFlag(f Flag, sig os.Signal) { + flags[f] = sig +} + +// SendCommands sends active signals to the given process. +func SendCommands(p *os.Process) (err error) { + for _, sig := range signals() { + if err = p.Signal(sig); err != nil { + return + } + } + return +} + +// ActiveFlags returns flags that has the state 'set'. +func ActiveFlags() (ret []Flag) { + ret = make([]Flag, 0, 1) + for f := range flags { + if f.IsSet() { + ret = append(ret, f) + } + } + return +} + +func signals() (ret []os.Signal) { + ret = make([]os.Signal, 0, 1) + for f, sig := range flags { + if f.IsSet() { + ret = append(ret, sig) + } + } + return +} diff --git a/daemon/daemon_linux.go b/daemon/daemon_linux.go new file mode 100644 index 0000000..d89160a --- /dev/null +++ b/daemon/daemon_linux.go @@ -0,0 +1,266 @@ +package daemon + +import ( + "encoding/json" + "fmt" + "os" + "syscall" + + "github.com/kardianos/osext" +) + +// Mark of daemon process - system environment variable _GO_DAEMON=1 +const ( + MARK_NAME = "_GO_DAEMON" + MARK_VALUE = "1" +) + +// Default file permissions for log and pid files. +const FILE_PERM = os.FileMode(0640) + +// A Context describes daemon context. +type Context struct { + // If PidFileName is non-empty, parent process will try to create and lock + // pid file with given name. Child process writes process id to file. + PidFileName string + // Permissions for new pid file. + PidFilePerm os.FileMode + + // If LogFileName is non-empty, parent process will create file with given name + // and will link to fd 2 (stderr) for child process. + LogFileName string + // Permissions for new log file. + LogFilePerm os.FileMode + + // If WorkDir is non-empty, the child changes into the directory before + // creating the process. + WorkDir string + // If Chroot is non-empty, the child changes root directory + Chroot string + + // If Env is non-nil, it gives the environment variables for the + // daemon-process in the form returned by os.Environ. + // If it is nil, the result of os.Environ will be used. + Env []string + // If Args is non-nil, it gives the command-line args for the + // daemon-process. If it is nil, the result of os.Args will be used + // (without program name). + Args []string + + // Credential holds user and group identities to be assumed by a daemon-process. + Credential *syscall.Credential + // If Umask is non-zero, the daemon-process call Umask() func with given value. + Umask int + + // Struct contains only serializable public fields (!!!) + abspath string + pidFile *LockFile + logFile *os.File + nullFile *os.File + + rpipe, wpipe *os.File +} + +// Reborn runs second copy of current process in the given context. +// function executes separate parts of code in child process and parent process +// and provides demonization of child process. It look similar as the +// fork-daemonization, but goroutine-safe. +// In success returns *os.Process in parent process and nil in child process. +// Otherwise returns error. +func (d *Context) Reborn() (child *os.Process, err error) { + if !WasReborn() { + child, err = d.parent() + } else { + err = d.child() + } + return +} + +// Search search daemons process by given in context pid file name. +// If success returns pointer on daemons os.Process structure, +// else returns error. Returns nil if filename is empty. +func (d *Context) Search() (daemon *os.Process, err error) { + if len(d.PidFileName) > 0 { + var pid int + if pid, err = ReadPidFile(d.PidFileName); err != nil { + return + } + daemon, err = os.FindProcess(pid) + } + return +} + +// WasReborn returns true in child process (daemon) and false in parent process. +func WasReborn() bool { + return os.Getenv(MARK_NAME) == MARK_VALUE +} + +func (d *Context) parent() (child *os.Process, err error) { + if err = d.prepareEnv(); err != nil { + return + } + + defer d.closeFiles() + if err = d.openFiles(); err != nil { + return + } + + attr := &os.ProcAttr{ + Dir: d.WorkDir, + Env: d.Env, + Files: d.files(), + Sys: &syscall.SysProcAttr{ + //Chroot: d.Chroot, + Credential: d.Credential, + Setsid: true, + }, + } + + if child, err = os.StartProcess(d.abspath, d.Args, attr); err != nil { + if d.pidFile != nil { + d.pidFile.Remove() + } + return + } + + d.rpipe.Close() + encoder := json.NewEncoder(d.wpipe) + err = encoder.Encode(d) + + return +} + +func (d *Context) openFiles() (err error) { + if d.PidFilePerm == 0 { + d.PidFilePerm = FILE_PERM + } + if d.LogFilePerm == 0 { + d.LogFilePerm = FILE_PERM + } + + if d.nullFile, err = os.Open(os.DevNull); err != nil { + return + } + + if len(d.PidFileName) > 0 { + if d.pidFile, err = OpenLockFile(d.PidFileName, d.PidFilePerm); err != nil { + return + } + if err = d.pidFile.Lock(); err != nil { + return + } + } + + if len(d.LogFileName) > 0 { + if d.logFile, err = os.OpenFile(d.LogFileName, + os.O_WRONLY|os.O_CREATE|os.O_APPEND, d.LogFilePerm); err != nil { + return + } + } + + d.rpipe, d.wpipe, err = os.Pipe() + return +} + +func (d *Context) closeFiles() (err error) { + cl := func(file **os.File) { + if *file != nil { + (*file).Close() + *file = nil + } + } + cl(&d.rpipe) + cl(&d.wpipe) + cl(&d.logFile) + cl(&d.nullFile) + if d.pidFile != nil { + d.pidFile.Close() + d.pidFile = nil + } + return +} + +func (d *Context) prepareEnv() (err error) { + if d.abspath, err = osext.Executable(); err != nil { + return + } + + if len(d.Args) == 0 { + d.Args = os.Args + } + + mark := fmt.Sprintf("%s=%s", MARK_NAME, MARK_VALUE) + if len(d.Env) == 0 { + d.Env = os.Environ() + } + d.Env = append(d.Env, mark) + + return +} + +func (d *Context) files() (f []*os.File) { + log := d.nullFile + if d.logFile != nil { + log = d.logFile + } + + f = []*os.File{ + d.rpipe, // (0) stdin + log, // (1) stdout + log, // (2) stderr + d.nullFile, // (3) dup on fd 0 after initialization + } + + if d.pidFile != nil { + f = append(f, d.pidFile.File) // (4) pid file + } + return +} + +var initialized = false + +func (d *Context) child() (err error) { + if initialized { + return os.ErrInvalid + } + initialized = true + + decoder := json.NewDecoder(os.Stdin) + if err = decoder.Decode(d); err != nil { + return + } + + if err = syscall.Close(0); err != nil { + return + } + if err = syscall.Dup2(3, 0); err != nil { + return + } + + if len(d.PidFileName) > 0 { + d.pidFile = NewLockFile(os.NewFile(4, d.PidFileName)) + if err = d.pidFile.WritePid(); err != nil { + return + } + } + + if d.Umask != 0 { + syscall.Umask(int(d.Umask)) + } + if len(d.Chroot) > 0 { + err = syscall.Chroot(d.Chroot) + } + + return +} + +// Release provides correct pid-file release in daemon. +func (d *Context) Release() (err error) { + if !initialized { + return + } + if d.pidFile != nil { + err = d.pidFile.Remove() + } + return +} diff --git a/daemon/daemon_windows.go b/daemon/daemon_windows.go new file mode 100644 index 0000000..7c7f458 --- /dev/null +++ b/daemon/daemon_windows.go @@ -0,0 +1,70 @@ +package daemon + +import ( + "os" +) + +// A Context describes daemon context. +type Context struct { + // If PidFileName is non-empty, parent process will try to create and lock + // pid file with given name. Child process writes process id to file. + PidFileName string + // Permissions for new pid file. + PidFilePerm os.FileMode + + // If LogFileName is non-empty, parent process will create file with given name + // and will link to fd 2 (stderr) for child process. + LogFileName string + // Permissions for new log file. + LogFilePerm os.FileMode + + // If WorkDir is non-empty, the child changes into the directory before + // creating the process. + WorkDir string + // If Chroot is non-empty, the child changes root directory + Chroot string + + // If Env is non-nil, it gives the environment variables for the + // daemon-process in the form returned by os.Environ. + // If it is nil, the result of os.Environ will be used. + Env []string + // If Args is non-nil, it gives the command-line args for the + // daemon-process. If it is nil, the result of os.Args will be used + // (without program name). + Args []string + + // Credential holds user and group identities to be assumed by a daemon-process. + Credential *int + // If Umask is non-zero, the daemon-process call Umask() func with given value. + Umask int + + // Struct contains only serializable public fields (!!!) + abspath string + pidFile *int + logFile *os.File + nullFile *os.File + + rpipe, wpipe *os.File +} + +// Reborn runs second copy of current process in the given context. +// function executes separate parts of code in child process and parent process +// and provides demonization of child process. It look similar as the +// fork-daemonization, but goroutine-safe. +// In success returns *os.Process in parent process and nil in child process. +// Otherwise returns error. +func (d *Context) Reborn() (child *os.Process, err error) { + return +} + +// Search search daemons process by given in context pid file name. +// If success returns pointer on daemons os.Process structure, +// else returns error. Returns nil if filename is empty. +func (d *Context) Search() (daemon *os.Process, err error) { + return +} + +// Release provides correct pid-file release in daemon. +func (d *Context) Release() (err error) { + return +} diff --git a/daemon/lock_file_linux.go b/daemon/lock_file_linux.go new file mode 100644 index 0000000..095eabb --- /dev/null +++ b/daemon/lock_file_linux.go @@ -0,0 +1,134 @@ +package daemon + +import ( + "fmt" + "os" + "syscall" +) + +var ( + // ErrWoldBlock indicates on locking pid-file by another process. + ErrWouldBlock = syscall.EWOULDBLOCK +) + +// LockFile wraps *os.File and provide functions for locking of files. +type LockFile struct { + *os.File +} + +// NewLockFile returns a new LockFile with the given File. +func NewLockFile(file *os.File) *LockFile { + return &LockFile{file} +} + +// CreatePidFile opens the named file, applies exclusive lock and writes +// current process id to file. +func CreatePidFile(name string, perm os.FileMode) (lock *LockFile, err error) { + if lock, err = OpenLockFile(name, perm); err != nil { + return + } + if err = lock.Lock(); err != nil { + lock.Remove() + return + } + if err = lock.WritePid(); err != nil { + lock.Remove() + } + return +} + +// OpenLockFile opens the named file with flags os.O_RDWR|os.O_CREATE and specified perm. +// If successful, function returns LockFile for opened file. +func OpenLockFile(name string, perm os.FileMode) (lock *LockFile, err error) { + var file *os.File + if file, err = os.OpenFile(name, os.O_RDWR|os.O_CREATE, perm); err == nil { + lock = &LockFile{file} + } + return +} + +// Lock apply exclusive lock on an open file. If file already locked, returns error. +func (file *LockFile) Lock() error { + return syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) +} + +// Unlock remove exclusive lock on an open file. +func (file *LockFile) Unlock() error { + return syscall.Flock(int(file.Fd()), syscall.LOCK_UN) +} + +// ReadPidFile reads process id from file with give name and returns pid. +// If unable read from a file, returns error. +func ReadPidFile(name string) (pid int, err error) { + var file *os.File + if file, err = os.OpenFile(name, os.O_RDONLY, 0640); err != nil { + return + } + defer file.Close() + + lock := &LockFile{file} + pid, err = lock.ReadPid() + return +} + +// WritePid writes current process id to an open file. +func (file *LockFile) WritePid() (err error) { + if _, err = file.Seek(0, os.SEEK_SET); err != nil { + return + } + var fileLen int + if fileLen, err = fmt.Fprint(file, os.Getpid()); err != nil { + return + } + if err = file.Truncate(int64(fileLen)); err != nil { + return + } + err = file.Sync() + return +} + +// ReadPid reads process id from file and returns pid. +// If unable read from a file, returns error. +func (file *LockFile) ReadPid() (pid int, err error) { + if _, err = file.Seek(0, os.SEEK_SET); err != nil { + return + } + _, err = fmt.Fscan(file, &pid) + return +} + +// Remove removes lock, closes and removes an open file. +func (file *LockFile) Remove() error { + defer file.Close() + + if err := file.Unlock(); err != nil { + return err + } + + name, err := GetFdName(file.Fd()) + if err != nil { + return err + } + + err = syscall.Unlink(name) + return err +} + +// GetFdName returns file name for given descriptor. +func GetFdName(fd uintptr) (name string, err error) { + path := fmt.Sprintf("/proc/self/fd/%d", int(fd)) + + var ( + fi os.FileInfo + n int + ) + if fi, err = os.Lstat(path); err != nil { + return + } + buf := make([]byte, fi.Size()+1) + + if n, err = syscall.Readlink(path, buf); err == nil { + name = string(buf[:n]) + } + return +} diff --git a/daemon/signal_linux.go b/daemon/signal_linux.go new file mode 100644 index 0000000..82021cb --- /dev/null +++ b/daemon/signal_linux.go @@ -0,0 +1,59 @@ +package daemon + +import ( + "errors" + "os" + "os/signal" + "syscall" +) + +// ErrStop should be returned signal handler function +// for termination of handling signals. +var ErrStop = errors.New("stop serve signals") + +// SignalHandlerFunc is the interface for signal handler functions. +type SignalHandlerFunc func(sig os.Signal) (err error) + +// func SetSigHandler sets handler for the given signals. +// SIGTERM has the default handler, he returns ErrStop. +func SetSigHandler(handler SignalHandlerFunc, signals ...os.Signal) { + for _, sig := range signals { + handlers[sig] = handler + } +} + +// func ServeSignals calls handlers for system signals. +func ServeSignals() (err error) { + signals := make([]os.Signal, 0, len(handlers)) + for sig, _ := range handlers { + signals = append(signals, sig) + } + + ch := make(chan os.Signal, 8) + signal.Notify(ch, signals...) + + for sig := range ch { + err = handlers[sig](sig) + if err != nil { + break + } + } + + signal.Stop(ch) + + if err == ErrStop { + err = nil + } + + return +} + +var handlers = make(map[os.Signal]SignalHandlerFunc) + +func init() { + handlers[syscall.SIGTERM] = sigtermDefaultHandler +} + +func sigtermDefaultHandler(sig os.Signal) error { + return ErrStop +} diff --git a/goicy.go b/goicy.go index ed64aae..10c34b2 100644 --- a/goicy.go +++ b/goicy.go @@ -3,12 +3,15 @@ package main import ( "fmt" "github.com/stunndard/goicy/config" + "github.com/stunndard/goicy/daemon" "github.com/stunndard/goicy/logger" "github.com/stunndard/goicy/playlist" "github.com/stunndard/goicy/stream" "github.com/stunndard/goicy/util" + "os" "os/signal" + "runtime" "syscall" "time" ) @@ -49,7 +52,34 @@ func main() { logger.File("goicy v"+config.Version+" started", logger.LOG_INFO) logger.Log("Loaded config file: "+inifile, logger.LOG_INFO) - defer logger.Log("exiting", logger.LOG_INFO) + // daemonizing + if config.Cfg.IsDaemon && runtime.GOOS == "linux" { + logger.Log("Daemon mode, detaching from terminal...", logger.LOG_INFO) + + cntxt := &daemon.Context{ + PidFileName: config.Cfg.PidFile, + PidFilePerm: 0644, + //LogFileName: "log", + //LogFilePerm: 0640, + WorkDir: "./", + Umask: 027, + //Args: []string{"[goicy sample]"}, + } + + d, err := cntxt.Reborn() + if err != nil { + logger.File(err.Error(), logger.LOG_ERROR) + return + } + if d != nil { + logger.File("Parent process died", logger.LOG_INFO) + return + } + defer cntxt.Release() + logger.Log("Daemonized successfully", logger.LOG_INFO) + } + + defer logger.Log("goicy exiting", logger.LOG_INFO) if err := playlist.Load(); err != nil { logger.Log("Cannot load playlist file", logger.LOG_ERROR) diff --git a/goicy.ini b/goicy.ini index 14bce87..bb6178d 100644 --- a/goicy.ini +++ b/goicy.ini @@ -93,6 +93,15 @@ playrandom = 0 [misc] +; daemon mode, works on linux only. +; 1 to enable, 0 to disable +; ignored totally on windows +daemon = 1 + +; pid file for the goicy daemon. works on linux only +; ignored totally on windows +pidfile = /var/run/goicy.pid + ; send-ahead buffer size in seconds buffersize = 3