diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/daemon.go b/daemon.go new file mode 100644 index 0000000..c7e8dd7 --- /dev/null +++ b/daemon.go @@ -0,0 +1,131 @@ +// Copyright 2014 Igor Dolzhikov. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package daemon 0.1.0 for use with Go (golang) services. +// +// Package daemon provides primitives for daemonization of golang services. +// This package is not provide implementation of user daemon, +// accordingly must have root rights to install/remove service. +// In the current implementation is only supported Linux and Mac Os X daemon. +// +// Example: +// +// package main +// +// import ( +// "fmt" +// "os" +// "github.com/takama/daemon" +// ) +// +// const ( +// name = "myservice" +// description = "Some explanation of the service purpose" +// ) +// +// type Service struct { +// daemon.Daemon +// } +// +// func (service *Service) Manage() (string, error) { +// // if received any kind of command, do it +// if len(os.Args) > 1 { +// command := os.Args[1] +// switch command { +// case "install": +// return service.Install() +// case "remove": +// return service.Remove() +// case "start": +// return service.Start() +// case "stop": +// return service.Stop() +// case "status": +// return service.Status() +// } +// } +// +// // Do something, call your goroutines, etc +// +// return "Usage: myservice install | remove | start | stop | status", nil +// } +// +// func main() { +// srv, err := daemon.New(name, description) +// if err != nil { +// fmt.Println("Error: ", err) +// os.Exit(1) +// } +// service := &Service{srv} +// status, err := service.Manage() +// if err != nil { +// fmt.Println(status, "\nError: ", err) +// os.Exit(1) +// } +// fmt.Println(status) +// } +// +// Go daemon +package daemon + +import ( + "os" + "os/exec" + "os/user" +) + +// Service constants +const ( + rootPrivileges = "You must have root user privileges. Possibly using 'sudo' command should help" + success = "\t\t\t\t\t[ \033[32mOK\033[0m ]" // Show colored "OK" + failed = "\t\t\t\t\t[\033[31mFAILED\033[0m]" // Show colored "FAILED" +) + +// Daemon interface has standard set of a methods/commands +type Daemon interface { + + // Install the service into the system + Install() (string, error) + + // Remove the service and all corresponded files from the system + Remove() (string, error) + + // Start the service + Start() (string, error) + + // Stop the service + Stop() (string, error) + + // Status - check the service status + Status() (string, error) +} + +// New - Create a new daemon +// +// name: name ot the service, must be match with executable file name; +// description: any explanation, what is the service, its purpose +func New(name, description string) (Daemon, error) { + return newDaemon(name, description) +} + +// Lookup path for executable file +func executablePath(name string) (string, error) { + if path, err := exec.LookPath(name); err == nil { + _, err := os.Stat(path) + if os.IsNotExist(err) { + return execPath() + } + return path, nil + } + return execPath() +} + +// Check root rights to use system service +func checkPrivileges() bool { + + if user, err := user.Current(); err == nil && user.Gid == "0" { + return true + } + return false +} diff --git a/darwin.go b/darwin.go new file mode 100644 index 0000000..6b830ec --- /dev/null +++ b/darwin.go @@ -0,0 +1,210 @@ +// Copyright 2014 Igor Dolzhikov. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package daemon darwin (mac os x) version +package daemon + +import ( + "errors" + "os" + "os/exec" + "path/filepath" + "regexp" + "text/template" +) + +// DarwinRecord - standard record (struct) for darwin version of daemon package +type DarwinRecord struct { + name string + description string +} + +func newDaemon(name, description string) (*DarwinRecord, error) { + + return &DarwinRecord{name, description}, nil +} + +// Standard service path for system daemons +func (darwin *DarwinRecord) servicePath() string { + return "/Library/LaunchDaemons/" + darwin.name + ".plist" +} + +// Check service is installed +func (darwin *DarwinRecord) checkInstalled() bool { + + if _, err := os.Stat(darwin.servicePath()); err == nil { + return true + } + + return false +} + +// Get executable path +func execPath() (string, error) { + return filepath.Abs(os.Args[0]) +} + +// Check service is running +func (darwin *DarwinRecord) checkRunning() (string, bool) { + output, err := exec.Command("launchctl", "list", darwin.name).Output() + if err == nil { + if matched, err := regexp.MatchString(darwin.name, string(output)); err == nil && matched { + reg := regexp.MustCompile("PID\" = ([0-9]+);") + data := reg.FindStringSubmatch(string(output)) + if len(data) > 1 { + return "Service (pid " + data[1] + ") is running...", true + } + return "Service is running...", true + } + } + + return "Service is stoped", false +} + +// Install the service +func (darwin *DarwinRecord) Install() (string, error) { + installAction := "Install " + darwin.description + ":" + + if checkPrivileges() == false { + return installAction + failed, errors.New(rootPrivileges) + } + + srvPath := darwin.servicePath() + + if darwin.checkInstalled() == true { + return installAction + failed, errors.New(darwin.description + " already installed") + } + + file, err := os.Create(srvPath) + if err != nil { + return installAction + failed, err + } + defer file.Close() + + execPatch, err := executablePath(darwin.name) + if err != nil { + return installAction + failed, err + } + + templ, err := template.New("propertyList").Parse(propertyList) + if err != nil { + return installAction + failed, err + } + + if err := templ.Execute( + file, + &struct { + Name, Path string + }{darwin.name, execPatch}, + ); err != nil { + return installAction + failed, err + } + + return installAction + success, nil +} + +// Remove the service +func (darwin *DarwinRecord) Remove() (string, error) { + removeAction := "Removing " + darwin.description + ":" + + if checkPrivileges() == false { + return removeAction + failed, errors.New(rootPrivileges) + } + + if darwin.checkInstalled() == false { + return removeAction + failed, errors.New(darwin.description + " not installed") + } + + if err := os.Remove(darwin.servicePath()); err != nil { + return removeAction + failed, err + } + + return removeAction + success, nil +} + +// Start the service +func (darwin *DarwinRecord) Start() (string, error) { + startAction := "Starting " + darwin.description + ":" + + if checkPrivileges() == false { + return startAction + failed, errors.New(rootPrivileges) + } + + if darwin.checkInstalled() == false { + return startAction + failed, errors.New(darwin.description + " not installed") + } + + if _, status := darwin.checkRunning(); status == true { + return startAction + failed, errors.New("service already running") + } + + if err := exec.Command("launchctl", "load", darwin.servicePath()).Run(); err != nil { + return startAction + failed, err + } + + return startAction + success, nil +} + +// Stop the service +func (darwin *DarwinRecord) Stop() (string, error) { + stopAction := "Stopping " + darwin.description + ":" + + if checkPrivileges() == false { + return stopAction + failed, errors.New(rootPrivileges) + } + + if darwin.checkInstalled() == false { + return stopAction + failed, errors.New(darwin.description + " not installed") + } + + if _, status := darwin.checkRunning(); status == false { + return stopAction + failed, errors.New("service already stopped") + } + + if err := exec.Command("launchctl", "unload", darwin.servicePath()).Run(); err != nil { + return stopAction + failed, err + } + + return stopAction + success, nil +} + +// Status - Get service status +func (darwin *DarwinRecord) Status() (string, error) { + + if checkPrivileges() == false { + return "", errors.New(rootPrivileges) + } + + if darwin.checkInstalled() == false { + return "Status could not defined", errors.New(darwin.description + " not installed") + } + + statusAction, _ := darwin.checkRunning() + + return statusAction, nil +} + +var propertyList = ` + + + + KeepAlive + + Label + {{.Name}} + ProgramArguments + + {{.Path}} + + RunAtLoad + + WorkingDirectory + /usr/local/var + StandardErrorPath + /usr/local/var/log/{{.Name}}.err + StandardOutPath + /usr/local/var/log/{{.Name}}.log + + +` diff --git a/example/myservice.go b/example/myservice.go new file mode 100644 index 0000000..5caad54 --- /dev/null +++ b/example/myservice.go @@ -0,0 +1,56 @@ +package main + +import ( + "fmt" + "os" + + "github.com/takama/daemon" +) + +const ( + name = "myservice" + description = "Some explanation of the service purpose" +) + +type Service struct { + daemon.Daemon +} + +func (service *Service) Manage() (string, error) { + // if received any kind of command, do it + if len(os.Args) > 1 { + command := os.Args[1] + switch command { + case "install": + return service.Install() + case "remove": + return service.Remove() + case "start": + return service.Start() + case "stop": + return service.Stop() + case "status": + return service.Status() + } + } + + // Do something, call your goroutines, etc + + return "Usage: myservice install | remove | start | stop | status", nil +} + +func main() { + srv, err := daemon.New(name, description) + if err != nil { + fmt.Println("Error: ", err) + os.Exit(1) + } + service := &Service{srv} + status, err := service.Manage() + if err != nil { + fmt.Println(status, "\nError: ", err) + os.Exit(1) + } + fmt.Println(status) + +} diff --git a/linux.go b/linux.go new file mode 100644 index 0000000..7a42b07 --- /dev/null +++ b/linux.go @@ -0,0 +1,312 @@ +// Copyright 2014 Igor Dolzhikov. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package daemon linux version +package daemon + +import ( + "errors" + "os" + "os/exec" + "regexp" + "text/template" +) + +// LinuxRecord - standard record (struct) for linux version of daemon package +type LinuxRecord struct { + name string + description string +} + +func newDaemon(name, description string) (*LinuxRecord, error) { + + return &LinuxRecord{name, description}, nil +} + +// Standard service path for system V daemons +func (linux *LinuxRecord) servicePath() string { + return "/etc/init.d/" + linux.name +} + +// Check service is installed +func (linux *LinuxRecord) checkInstalled() bool { + + if _, err := os.Stat(linux.servicePath()); err == nil { + return true + } + + return false +} + +// Get executable path +func execPath() (string, error) { + return os.Readlink("/proc/self/exe") +} + +// Check service is running +func (linux *LinuxRecord) checkRunning() (string, bool) { + output, err := exec.Command("service", linux.name, "status").Output() + if err == nil { + if matched, err := regexp.MatchString(linux.name, string(output)); err == nil && matched { + reg := regexp.MustCompile("pid ([0-9]+)") + data := reg.FindStringSubmatch(string(output)) + if len(data) > 1 { + return "Service (pid " + data[1] + ") is running...", true + } + return "Service is running...", true + } + } + + return "Service is stoped", false +} + +// Install the service +func (linux *LinuxRecord) Install() (string, error) { + installAction := "Install " + linux.description + ":" + + if checkPrivileges() == false { + return installAction + failed, errors.New(rootPrivileges) + } + + srvPath := linux.servicePath() + + if linux.checkInstalled() == true { + return installAction + failed, errors.New(linux.description + " already installed") + } + + file, err := os.Create(srvPath) + if err != nil { + return installAction + failed, err + } + defer file.Close() + + execPatch, err := executablePath(linux.name) + if err != nil { + return installAction + failed, err + } + + templ, err := template.New("daemonConfig").Parse(daemonConfig) + if err != nil { + return installAction + failed, err + } + + if err := templ.Execute( + file, + &struct { + Name, Description, Path string + }{linux.name, linux.description, execPatch}, + ); err != nil { + return installAction + failed, err + } + + if err := os.Chmod(srvPath, 0755); err != nil { + return installAction + failed, err + } + + for _, i := range [...]string{"2", "3", "4", "5"} { + if err := os.Symlink(srvPath, "/etc/rc"+i+".d/S87"+linux.name); err != nil { + continue + } + } + for _, i := range [...]string{"0", "1", "6"} { + if err := os.Symlink(srvPath, "/etc/rc"+i+".d/K17"+linux.name); err != nil { + continue + } + } + + return installAction + success, nil +} + +// Remove the service +func (linux *LinuxRecord) Remove() (string, error) { + removeAction := "Removing " + linux.description + ":" + + if checkPrivileges() == false { + return removeAction + failed, errors.New(rootPrivileges) + } + + if linux.checkInstalled() == false { + return removeAction + failed, errors.New(linux.description + " not installed") + } + + if err := os.Remove(linux.servicePath()); err != nil { + return removeAction + failed, err + } + + for _, i := range [...]string{"2", "3", "4", "5"} { + if err := os.Remove("/etc/rc" + i + ".d/S87" + linux.name); err != nil { + continue + } + } + for _, i := range [...]string{"0", "1", "6"} { + if err := os.Remove("/etc/rc" + i + ".d/K17" + linux.name); err != nil { + continue + } + } + + return removeAction + success, nil +} + +// Start the service +func (linux *LinuxRecord) Start() (string, error) { + startAction := "Starting " + linux.description + ":" + + if checkPrivileges() == false { + return startAction + failed, errors.New(rootPrivileges) + } + + if linux.checkInstalled() == false { + return startAction + failed, errors.New(linux.description + " not installed") + } + + if _, status := linux.checkRunning(); status == true { + return startAction + failed, errors.New("service already running") + } + + if err := exec.Command("service", linux.name, "start").Run(); err != nil { + return startAction + failed, err + } + + return startAction + success, nil +} + +// Stop the service +func (linux *LinuxRecord) Stop() (string, error) { + stopAction := "Stopping " + linux.description + ":" + + if checkPrivileges() == false { + return stopAction + failed, errors.New(rootPrivileges) + } + + if linux.checkInstalled() == false { + return stopAction + failed, errors.New(linux.description + " not installed") + } + + if _, status := linux.checkRunning(); status == false { + return stopAction + failed, errors.New("service already stopped") + } + + if err := exec.Command("service", linux.name, "stop").Run(); err != nil { + return stopAction + failed, err + } + + return stopAction + success, nil +} + +// Status - Get service status +func (linux *LinuxRecord) Status() (string, error) { + + if checkPrivileges() == false { + return "", errors.New(rootPrivileges) + } + + if linux.checkInstalled() == false { + return "Status could not defined", errors.New(linux.description + " not installed") + } + + statusAction, _ := linux.checkRunning() + + return statusAction, nil +} + +var daemonConfig = `#! /bin/sh +# +# /etc/rc.d/init.d/{{.Name}} +# +# Starts {{.Name}} as a daemon +# +# chkconfig: 2345 87 17 +# description: Starts and stops a single {{.Name}} instance on this system + +### BEGIN INIT INFO +# Provides: {{.Name}} +# Required-Start: $network $named +# Required-Stop: $network $named +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: This service manages the {{.Description}}. +# Description: {{.Description}} +### END INIT INFO + +# +# Source function library. +# +if [ -f /etc/rc.d/init.d/functions ]; then + . /etc/rc.d/init.d/functions +fi + +exec="{{.Path}}" +servname="{{.Description}}" + +proc=$(basename $0) +pidfile="/var/run/$proc.pid" +lockfile="/var/lock/subsys/$proc" +stdoutlog="/var/log/$proc.log" +stderrlog="/var/log/$proc.err" + +[ -e /etc/sysconfig/$proc ] && . /etc/sysconfig/$proc + +start() { + [ -x $exec ] || exit 5 + + if ! [ -f $pidfile ]; then + printf "Starting $servname:\t" + echo "$(date)" >> $stdoutlog + $exec >> $stderrlog 2>> $stdoutlog & + echo $! > $pidfile + touch $lockfile + success + echo + else + failure + echo + printf "$pidfile still exists...\n" + exit 7 + fi +} + +stop() { + echo -n $"Stopping $servname: " + killproc -p $pidfile $proc + retval=$? + echo + [ $retval -eq 0 ] && rm -f $lockfile + return $retval +} + +restart() { + stop + start +} + +rh_status() { + status -p $pidfile $proc +} + +rh_status_q() { + rh_status >/dev/null 2>&1 +} + +case "$1" in + start) + rh_status_q && exit 0 + $1 + ;; + stop) + rh_status_q || exit 0 + $1 + ;; + restart) + $1 + ;; + status) + rh_status + ;; + *) + echo $"Usage: $0 {start|stop|status|restart}" + exit 2 +esac + +exit $? +` diff --git a/windows.go b/windows.go new file mode 100644 index 0000000..a3f05fa --- /dev/null +++ b/windows.go @@ -0,0 +1,55 @@ +// Copyright 2014 Igor Dolzhikov. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package daemon windows version +package daemon + +import ( + "errors" +) + +// WindowsRecord - standard record (struct) for windows version of daemon package +type WindowsRecord struct { + name string + description string +} + +func newDaemon(name, description string) (*WindowsRecord, error) { + + return &WindowsRecord{name, description}, nil +} + +// Install the service +func (windows *WindowsRecord) Install() (string, error) { + installAction := "Install " + windows.description + ":" + + return installAction + failed, errors.New("Windows daemon not supported") +} + +// Remove the service +func (windows *WindowsRecord) Remove() (string, error) { + removeAction := "Removing " + windows.description + ":" + + return removeAction + failed, errors.New("Windows daemon not supported") +} + +// Start the service +func (windows *WindowsRecord) Start() (string, error) { + startAction := "Starting " + windows.description + ":" + + return startAction + failed, errors.New("Windows daemon not supported") +} + +// Stop the service +func (windows *WindowsRecord) Stop() (string, error) { + stopAction := "Stopping " + windows.description + ":" + + return stopAction + failed, errors.New("Windows daemon not supported") +} + +// Status - Get service status +func (windows *WindowsRecord) Status() (string, error) { + + return "Status could not defined", errors.New("Windows daemon not supported") +}