pax_global_header00006660000000000000000000000064127750013430014514gustar00rootroot0000000000000052 comment=b64b3467ebd16f64faec1640c25e318efc0c0d7b service-2.0.16/000077500000000000000000000000001277500134300132425ustar00rootroot00000000000000service-2.0.16/.travis.yml000066400000000000000000000005461277500134300153600ustar00rootroot00000000000000language: go os: - linux - osx go: - 1.4 - tip sudo: false addons: apt: packages: - libcap-dev install: - mkdir -p $HOME/gopath/src/gopkg.in/hlandau - mv $HOME/gopath/src/github.com/hlandau/service $HOME/gopath/src/gopkg.in/hlandau/service.v2 - cd $HOME/gopath/src/gopkg.in/hlandau/service.v2 - go get gopkg.in/hlandau/service.v2 service-2.0.16/README.md000066400000000000000000000240361277500134300145260ustar00rootroot00000000000000service: Write daemons in Go ============================ [![GoDoc](https://godoc.org/gopkg.in/hlandau/service.v2?status.svg)](https://godoc.org/gopkg.in/hlandau/service.v2) [![Build Status](https://travis-ci.org/hlandau/service.svg?branch=master)](https://travis-ci.org/hlandau/service) This package enables you to easily write services in Go such that the following concerns are taken care of automatically: - Daemonization - Fork emulation (not recommended, though) - PID file creation - Privilege dropping - Chrooting - Status notification (supports setproctitle and systemd notify protocol; this support is Go-native and does not introduce any dependency on any systemd library) - Operation as a Windows service - Orderly shutdown Standard Interface ------------------ Here's a usage example: ```go import "gopkg.in/hlandau/service.v2" import "gopkg.in/hlandau/easyconfig.v1" func main() { easyconfig.ParseFatal(nil, nil) service.Main(&service.Info{ Title: "Foobar Web Server", Name: "foobar", Description: "Foobar Web Server is the greatest webserver ever.", RunFunc: func(smgr service.Manager) error { // Start up your service. // ... // Once initialization requiring root is done, call this. err := smgr.DropPrivileges() if err != nil { return err } // When it is ready to serve requests, call this. // You must call DropPrivileges first. smgr.SetStarted() // Optionally set a status. smgr.SetStatus("foobar: running ok") // Wait until stop is requested. <-smgr.StopChan() // Do any necessary teardown. // ... // Done. return nil }, }) } ``` You should import the package as "gopkg.in/hlandau/service.v2". Compatibility will be preserved. (Please note that this compatibility guarantee does not extend to subpackages.) Simplified Interface -------------------- If you implement the following interface, you can use the simplified interface. This example also demonstrates how to use easyconfig to handle your configuration. ```go func() (Runnable, error) type Runnable interface { Start() error Stop() error } ``` Usage example: ```go import "gopkg.in/hlandau/service.v2" import "gopkg.in/hlandau/easyconfig.v1" type Config struct{} // Server which doesn't do anything type Server struct{} func New(cfg Config) (*Server, error) { // Instantiate the service and bind to ports here return &Server{}, nil } func (*Server) Start() error { // Start handling of requests here (must return) return nil } func (*Server) Stop() error { // Stop the service here return nil } func main() { cfg := Config{} easyconfig.Configurator{ ProgramName: "foobar", }.ParseFatal(&cfg) service.Main(&service.Info{ Name: "foobar", NewFunc: func() (service.Runnable, error) { return New(cfg) }, }) } ``` Changes since v1 ---------------- v1 used the "flag" package to register service configuration options like UID, GID, etc. v2 uses the "[configurable](https://github.com/hlandau/configurable)" package to register service configuration options. "configurable" is a neutral [integration nexus](http://www.devever.net/~hl/nexuses), so it increases the generality of `service`. However, bear in mind that you are responsible for ensuring that configuration is loaded before calling service.Main. Configurables ------------- The following configurables are automatically registered under a group configurable named "service": chroot (string) path (*nix only) chroot to a directory (must set UID, GID) ("/" disables) daemon (bool) (*nix only) run as daemon? (doesn't fork) (remaps stdin, stdout, stderr to /dev/null; calls setsid) fork (bool) (*nix only) fork? uid (string) username (*nix only) UID or username to setuid to gid (string) groupname (*nix only) GID or group name to setgid to pidfile (string) path (*nix only) Path of PID file to write and lock (default: no PID file) do (string) start|stop|install|remove (Windows only) Service control. cpuprofile (string) path Write CPU profile to file debugserveraddr (string) ip:port Bind the net/http DefaultServeMux to the given address (expvars, pprof handlers will be registered; intended for debug use only; set UsesDefaultHTTP in the Info type to disable the presence of this flag) If you call `easyconfig.ParseFatal(nil, nil)` as suggested above, these manifest as "flag" flags named -service.X, for each name X above. e.g. `-service.chroot=/` Using as a Windows service -------------------------- You can use the `-service.do=install` and `-service.do=remove` flags to install and remove the service as a Windows service. Please note that: - You will need to run these commands from an elevated command prompt (right click on 'Command Prompt' and select 'Run as administrator'). - The absolute path of the executable in its current location will be used as the path to the service. - You may need to tweak the command line arguments for the service to your liking using `services.msc` after installation. - You may also use any other method that you like to install or remove services. No particular command line flag is required; the service will detect when it is being run as a Windows service automatically. ### Manifests If your service *always* needs to run privileged, you may want to apply a manifest file to your binary to make elevation automatic. You should avoid this if your service can be configured to usefully operate without elevation, as it denies the user choice in how to run the service. Here is an example manifest: ```xml ``` You can use this manifest either as a sidecar file by naming it `.exe.manifest`, or by embedding it into the binary. You may wish to investigate Microsoft's `mt` tool or [akavel/rsrc](https://github.com/akavel/rsrc), which provides a Go-specific solution. For more information on manifests, see MSDN. Use with systemd ---------------- Here is an example systemd unit file with privilege dropping and auto-restart: [Unit] Description=short description of the daemon ;; Optionally make the service dependent on other services ;Requires=other.service [Service] Type=notify ExecStart=/path/to/foobar/foobard -service.uid=foobar -service.gid=foobar -service.daemon=1 Restart=always RestartSec=30 [Install] WantedBy=multi-user.target Bugs ---- - If you don't consume registered configurables, the user cannot configure the options of this package, rendering it somewhat unusable. You must handle registered configurables. The easyconfig example above suffices. - Testing would be nice, but a library of this nature isn't too susceptible to unit testing. Something to think about. - **Severe**: A bug in Go 1.5 means that privilege dropping does not work correctly, but instead hangs forever ([#12498](https://github.com/golang/go/issues/12498)). A patch is available but is not yet part of any release. As a workaround, use Go 1.4 or do not use privilege dropping (e.g. run as a non-root user and do not specify `-uid`, `-gid` or `-chroot`). If you need to bind to low ports, you can use `setcap` on Linux to grant those privileges. (This bug is fixed in Go 1.5.2 and later.) Platform Support ---------------- The package should work on Windows or any UNIX-like platform, but has been tested only on the following platforms: - Linux - FreeBSD - Darwin/OS X - Windows On Linux **you may need to install the libcap development package** (`libcap-dev` on Debian-style distros, `libcap-devel` on Red Hat-style distros), as this package uses libcap to make sure all capabilities are dropped on Linux. Reduced Functionality Mode -------------------------- When built without cgo, the following limitations are imposed: - Privilege dropping is not supported at all on Linux. - UIDs and GIDs must be specified numerically, not as names. - No supplementary GIDs are configured when dropping privileges (the empty set is configured). - setproctitle is not supported; status setting is a no-op. Utility Library --------------- This package provides a simplified interface built on some functionality exposed in [hlandau/svcutils](https://github.com/hlandau/svcutils). People who want something less “magic” may find functions there useful. Some functions in that repository may still be useful to people using this package. For example, the chroot package allows you to (try to) relativize a path to a chroot, allowing you to address files by their absolute path after chrooting. Licence ------- ISC License Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. service-2.0.16/daemon/000077500000000000000000000000001277500134300145055ustar00rootroot00000000000000service-2.0.16/daemon/bansuid/000077500000000000000000000000001277500134300161325ustar00rootroot00000000000000service-2.0.16/daemon/bansuid/prctl.go000066400000000000000000000010451277500134300176050ustar00rootroot00000000000000// Package bansuid provides a function to prevent processes from reacquiring privileges. package bansuid import "errors" // On Linux, uses prctl() SECUREBITS and NO_NEW_PRIVS to prevent the process or its descendants // from ever obtaining privileges by execing a suid/sgid/cap xattr binary. Returns ErrNotSupported // if platform is not supported. May return other errors. func BanSuid() error { return banSuid() } // Returned by BanSuid if it is not supported on the current platform. var ErrNotSupported = errors.New("bansuid not supported") service-2.0.16/daemon/bansuid/prctl_linux.go000066400000000000000000000026341277500134300210310ustar00rootroot00000000000000// +build linux package bansuid import ( "fmt" "syscall" ) func banSuid() error { err := setNoNewPrivs() if err != nil { return err } // TODO: Consider use of capability bounding sets. // Though should be made unnecessary by NO_NEW_PRIVS. // Setting SECUREBITS requires capabilities we may not have if we are not running as root, // so we do this second. err = setSecurebits() if err != nil { return err } return nil } func setNoNewPrivs() error { err := prctl(pPR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) if err != nil { return fmt.Errorf("cannot set NO_NEW_PRIVS: %v", err) } return nil } func setSecurebits() error { err := prctl(pPR_SET_SECUREBITS, sSECBIT_NOROOT|sSECBIT_NOROOT_LOCKED|sSECBIT_KEEP_CAPS_LOCKED, 0, 0, 0) if err != nil { return fmt.Errorf("cannot set SECUREBITS: %v", err) } return nil } const ( pPR_SET_SECCOMP = 22 pPR_CAPBSET_DROP = 24 pPR_SET_SECUREBITS = 28 pPR_SET_NO_NEW_PRIVS = 36 sSECBIT_NOROOT = 1 << 0 sSECBIT_NOROOT_LOCKED = 1 << 1 sSECBIT_NO_SETUID_FIXUP = 1 << 2 sSECBIT_NO_SETUID_FIXUP_LOCKED = 1 << 3 sSECBIT_KEEP_CAPS = 1 << 4 sSECBIT_KEEP_CAPS_LOCKED = 1 << 5 ) func prctl(opt int, arg2, arg3, arg4, arg5 uint64) error { _, _, e1 := syscall.Syscall6(syscall.SYS_PRCTL, uintptr(opt), uintptr(arg2), uintptr(arg3), uintptr(arg4), uintptr(arg5), 0) if e1 != 0 { return e1 } return nil } service-2.0.16/daemon/bansuid/prctl_nlinux.go000066400000000000000000000001241277500134300211770ustar00rootroot00000000000000// +build !linux package bansuid func banSuid() error { return ErrNotSupported } service-2.0.16/daemon/daemon.go000066400000000000000000000055461277500134300163110ustar00rootroot00000000000000// +build !windows // Package daemon provides functions to assist with the writing of UNIX-style // daemons in go. package daemon import ( "gopkg.in/hlandau/svcutils.v1/dupfd" "gopkg.in/hlandau/svcutils.v1/exepath" "os" "syscall" ) // Initialises a daemon with recommended values. Called by Daemonize. // // Currently, this only calls umask(0) and chdir("/"). func Init() error { syscall.Umask(0) err := syscall.Chdir("/") if err != nil { return err } // setrlimit RLIMIT_CORE return nil } const forkedArg = "$*_FORKED_*$" // Psuedo-forks by re-executing the current binary with a special command line // argument telling it not to re-execute itself again. Returns true in the // parent process and false in the child. func Fork() (isParent bool, err error) { if os.Args[len(os.Args)-1] == forkedArg { os.Args = os.Args[0 : len(os.Args)-1] return false, nil } newArgs := make([]string, 0, len(os.Args)) newArgs = append(newArgs, exepath.Abs) newArgs = append(newArgs, os.Args[1:]...) newArgs = append(newArgs, forkedArg) // Start the child process. // // Pass along the standard FD for now - we'll remap them to /dev/null // in due time. This ensures anything expecting these to exist isn't confused, // and allows pre-daemonization failures to at least get output to somewhere. proc, err := os.StartProcess(exepath.Abs, newArgs, &os.ProcAttr{ Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}, }) if err != nil { return true, err } proc.Release() return true, nil } var haveStderr = true // Returns true unless stderr has been closed (remapped to /dev/null) as part // of daemonization. Can be used to determine whether logging to stderr is // useful. func HaveStderr() bool { return haveStderr } // Daemonizes but doesn't fork. // // The stdin, stdout and, unless keepStderr is specified, stderr fds are // remapped to /dev/null. setsid is called. // // The process changes its current directory to /. // // If you intend to call DropPrivileges, call it after calling this function, // as /dev/null will no longer be available after privileges are dropped. func Daemonize(keepStderr bool) error { null_f, err := os.OpenFile("/dev/null", os.O_RDWR, 0) if err != nil { return err } defer null_f.Close() stdin_fd := int(os.Stdin.Fd()) stdout_fd := int(os.Stdout.Fd()) stderr_fd := int(os.Stderr.Fd()) // ... reopen fds 0, 1, 2 as /dev/null ... // Since dup2 closes fds which are already open we needn't close the above fds. // This lets us avoid race conditions. null_fd := int(null_f.Fd()) err = dupfd.Dup2(null_fd, stdin_fd) if err != nil { return err } err = dupfd.Dup2(null_fd, stdout_fd) if err != nil { return err } if !keepStderr { err = dupfd.Dup2(null_fd, stderr_fd) if err != nil { return err } haveStderr = false } // This may fail if we're not root syscall.Setsid() // Daemonize implies Init. return Init() } service-2.0.16/daemon/droppriv.go000066400000000000000000000074251277500134300167110ustar00rootroot00000000000000// +build !windows package daemon import ( "errors" "fmt" "gopkg.in/hlandau/svcutils.v1/caps" "gopkg.in/hlandau/svcutils.v1/chroot" "gopkg.in/hlandau/svcutils.v1/passwd" "gopkg.in/hlandau/svcutils.v1/setuid" "net" "runtime" "sync" "syscall" ) // Drops privileges to the specified UID and GID. // This function does nothing and returns no error if all E?[UG]IDs are nonzero. // // If chrootDir is not empty, the process is chrooted into it. The directory // must exist. The function tests that privilege dropping has been successful // by attempting to setuid(0), which must fail. // // The current directory is set to / inside the chroot. // // The function ensures that /etc/hosts and /etc/resolv.conf are loaded before // chrooting, so name service should continue to be available. func DropPrivileges(UID, GID int, chrootDir string) (chrootErr error, err error) { // chroot and set UID and GIDs chrootErr, err = dropPrivileges(UID, GID, chrootDir) if err != nil { err = fmt.Errorf("dropPrivileges failed: %v", err) return } err = syscall.Chdir("/") if err != nil { return } err = ensureNoPrivs() if err != nil { err = fmt.Errorf("ensure no privs failed: %v", err) return } return } func dropPrivileges(UID, GID int, chrootDir string) (chrootErr error, err error) { if (UID <= 0) != (GID <= 0) { return nil, errors.New("either both or neither UID and GID must be set to positive (i.e. valid, non-root) values") } var gids []int if UID > 0 { gids, err = passwd.GetExtraGIDs(GID) if err != nil { return nil, err } gids = append(gids, GID) } chrootErr = tryChroot(chrootDir) if UID > 0 { err = tryDropPrivileges(UID, GID, gids) if err != nil { return } } return } var warnOnce sync.Once func tryDropPrivileges(UID, GID int, gids []int) error { if UID <= 0 || GID <= 0 { return errors.New("invalid UID/GID specified so cannot setuid/setgid") } if runtime.GOOS == "linux" { ver := runtime.Version() if ver == "go1.5" || ver == "go1.5.1" { return errors.New("It is not possible to drop privileges on Linux using Go 1.5 or 1.5.1 (Go bug #12498: ); either use Go1.4, 1.5.2 or a development branch of Go, or do not use privilege dropping by running services only as non-root users with no capabilities set") } } err := setuid.Setgroups(gids) if err != nil { return err } err = setuid.Setresgid(GID, GID, GID) if err != nil { return err } err = setuid.Setresuid(UID, UID, UID) if err != nil { return err } return nil } func tryChroot(path string) error { if path == "/" { path = "" } if path == "" { return nil } ensureResolverConfigIsLoaded() err := chroot.Chroot(path) if err != nil { return err } return nil } func ensureResolverConfigIsLoaded() { c, err := net.Dial("udp", "un_localhost:1") if err == nil { c.Close() } } func ensureNoPrivs() error { if IsRoot() { return errors.New("still have non-zero UID or GID or capabilities") } err := setuid.Setuid(0) if err == nil { return errors.New("Can't drop privileges - setuid(0) still succeeded") } err = setuid.Setgid(0) if err == nil { return errors.New("Can't drop privileges - setgid(0) still succeeded") } return nil } // Returns true if either or both of the following are true: // // Any of the UID, EUID, GID or EGID are zero. // // On supported platforms which support capabilities (currently Linux), any // capabilities are present. func IsRoot() bool { return caps.HaveAny() || isRoot() } func isRoot() bool { return syscall.Getuid() == 0 || syscall.Geteuid() == 0 || syscall.Getgid() == 0 || syscall.Getegid() == 0 } // This is set to a path which should be empty on the target platform. // // On Linux, the FHS provides that "/var/empty" should always be empty. var EmptyChrootPath = "/var/empty" service-2.0.16/gsptcall/000077500000000000000000000000001277500134300150535ustar00rootroot00000000000000service-2.0.16/gsptcall/gsptcall-cgo.go000066400000000000000000000002151277500134300177570ustar00rootroot00000000000000// +build cgo,unix package gsptcall import "github.com/erikdubbelboer/gspt" func setProcTitle(title string) { gspt.SetProcTitle(title) } service-2.0.16/gsptcall/gsptcall-nocgo.go000066400000000000000000000001141277500134300203120ustar00rootroot00000000000000// +build !cgo !unix package gsptcall func setProcTitle(title string) { } service-2.0.16/gsptcall/gsptcall.go000066400000000000000000000004651277500134300172200ustar00rootroot00000000000000// Package gsptcall provides a call wrapper for SetProcTitle which does nothing // when it is not supported. package gsptcall // Calls erikdubbelboer/gspt.SetProcTitle, but only on UNIX platforms and where // cgo is enabled. Otherwise, it is a no-op. func SetProcTitle(title string) { setProcTitle(title) } service-2.0.16/service.go000066400000000000000000000203221277500134300152300ustar00rootroot00000000000000// Package service wraps all the complexity of writing daemons while enabling // seamless integration with OS service management facilities. package service // import "gopkg.in/hlandau/service.v2" import ( "expvar" "fmt" "gopkg.in/hlandau/easyconfig.v1/cflag" "gopkg.in/hlandau/service.v2/gsptcall" "gopkg.in/hlandau/svcutils.v1/exepath" "io" "net/http" _ "net/http/pprof" // register pprof handler for debug server "os" "os/signal" "runtime/pprof" "sync" "syscall" "time" ) // Flags var ( fg = cflag.NewGroup(nil, "service") cpuProfileFlag = cflag.String(fg, "cpuprofile", "", "Write CPU profile to file") debugServerAddrFlag = cflag.String(fg, "debugserveraddr", "", "Address for debug server to listen on (do not specify a public address) (default: disabled)") ) type nullWriter struct{} func (nw nullWriter) Write(p []byte) (n int, err error) { return len(p), nil } func init() { expvar.NewString("service.startTime").Set(time.Now().String()) } // This function should typically be called directly from func main(). It takes // care of all housekeeping for running services and handles service lifecycle. func Main(info *Info) { info.main() } // The interface between the service library and the application-specific code. // The application calls the methods in the provided instance of this interface // at various stages in its lifecycle. type Manager interface { // Must be called when the service is ready to drop privileges. // This must be called before SetStarted(). DropPrivileges() error // Must be called by a service payload when it has finished starting. SetStarted() // A service payload must stop when this channel is closed. StopChan() <-chan struct{} // Called by a service payload to provide a single line of information on the // current status of that service. SetStatus(status string) } // Used only by the NewFunc interface. type Runnable interface { // Start the runnable. Any initialization requiring root privileges must // already have been obtained as this will be called after dropping // privileges. Must return. Start() error // Stop the runnable. Must return. Stop() error } // An upgrade interface for Runnable, implementation of which is optional. type StatusSource interface { // Return a channel on which status messages will be sent. If a Runnable // implements this, it is guaranteed that the channel will be consumed until // Stop is called. StatusChan() <-chan string } // An instantiable service. type Info struct { // Recommended. Codename for the service, e.g. "foobar" // // If this is not set, exepath.ProgramName is used, which by default is the // program's binary basename (e.g. "FooBar.exe" would become "foobar"). Name string // Required unless NewFunc is specified instead. Starts the service. Must not // return until the service has stopped. Must call smgr.SetStarted() to // indicate when it has finished starting and use smgr.StopChan() to // determine when to stop. // // Should call SetStatus() periodically with a status string. RunFunc func(smgr Manager) error // Optional. An alternative to RunFunc. If this is provided, RunFunc must not // be specified, and this package will provide its own implementation of // RunFunc. // // The NewFunc will be called to instantiate the runnable service. // Privileges will then be dropped and Start will be called. Start must // return. When the service is to be stopped, Stop will be called. Stop must // return. // // To implement status notification, implement also the StatusSource interface. NewFunc func() (Runnable, error) Title string // Optional. Friendly name for the service, e.g. "Foobar Web Server" Description string // Optional. Single line description for the service AllowRoot bool // May the service run as root? If false, the service will refuse to run as root unless privilege dropping is set. DefaultChroot string // Default path to chroot to. Use this if the service can be chrooted without consequence. NoBanSuid bool // Set to true if the ability to execute suid binaries must be retained. // Are we being started by systemd with [Service] Type=notify? // If so, we can issue service status notifications to systemd. systemd bool // Path to created PID file. pidFileName string pidFile io.Closer } func (info *Info) main() { err := info.maine() if err != nil { fmt.Fprintf(os.Stderr, "Error in service: %+v\n", err) os.Exit(1) } } func (info *Info) maine() error { if info.Name == "" { info.Name = exepath.ProgramName } else if exepath.ProgramNameSetter == "default" { exepath.ProgramName = info.Name exepath.ProgramNameSetter = "service" } if info.Name == "" { panic("service name must be specified") } if info.Title == "" { info.Title = info.Name } if info.Description == "" { info.Description = info.Title } err := info.commonPre() if err != nil { return err } err = info.setRunFunc() if err != nil { return err } // profiling if cpuProfileFlag.Value() != "" { f, err := os.Create(cpuProfileFlag.Value()) if err != nil { return err } pprof.StartCPUProfile(f) defer f.Close() defer pprof.StopCPUProfile() } err = info.serviceMain() return err } func (info *Info) commonPre() error { if debugServerAddrFlag != nil && debugServerAddrFlag.Value() != "" { go func() { err := http.ListenAndServe(debugServerAddrFlag.Value(), nil) if err != nil { fmt.Fprintf(os.Stderr, "Couldn't start debug server: %+v\n", err) } }() } return nil } func (info *Info) setRunFunc() error { if info.RunFunc != nil { return nil } if info.NewFunc == nil { panic("either RunFunc or NewFunc must be specified") } info.RunFunc = func(smgr Manager) error { // instantiate runnable r, err := info.NewFunc() if err != nil { return err } // setup status channel getStatusChan := func() <-chan string { return nil } if ss, ok := r.(StatusSource); ok { getStatusChan = func() <-chan string { return ss.StatusChan() } } // drop privileges err = smgr.DropPrivileges() if err != nil { return err } // start err = r.Start() if err != nil { return err } // smgr.SetStarted() smgr.SetStatus(info.Name + ": running ok") // wait for status messages or stop requests loop: for { select { case statusMsg := <-getStatusChan(): smgr.SetStatus(info.Name + ": " + statusMsg) case <-smgr.StopChan(): break loop } } // stop return r.Stop() } return nil } type ihandler struct { info *Info stopChan chan struct{} statusMutex sync.Mutex statusNotifyChan chan struct{} startedChan chan struct{} status string started bool stopping bool dropped bool } func (h *ihandler) SetStarted() { if !h.dropped { panic("service must call DropPrivileges before calling SetStarted") } select { case h.startedChan <- struct{}{}: default: } } func (h *ihandler) StopChan() <-chan struct{} { return h.stopChan } func (h *ihandler) SetStatus(status string) { h.statusMutex.Lock() h.status = status h.statusMutex.Unlock() select { case <-h.statusNotifyChan: default: } } func (h *ihandler) updateStatus() { // systemd if h.info.systemd { s := "" if h.started { s += "READY=1\n" } if h.status != "" { s += "STATUS=" + h.status + "\n" } systemdUpdateStatus(s) // ignore error } if h.status != "" { gsptcall.SetProcTitle(h.status) } } func (info *Info) runInteractively() error { smgr := ihandler{ info: info, stopChan: make(chan struct{}), statusNotifyChan: make(chan struct{}, 1), startedChan: make(chan struct{}, 1), } doneChan := make(chan error) go func() { err := info.RunFunc(&smgr) doneChan <- err }() sig := make(chan os.Signal) signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) var exitErr error loop: for { select { case <-sig: if !smgr.stopping { smgr.stopping = true close(smgr.stopChan) smgr.updateStatus() } case <-smgr.startedChan: if !smgr.started { smgr.started = true smgr.updateStatus() } case <-smgr.statusNotifyChan: smgr.updateStatus() case exitErr = <-doneChan: break loop } } return exitErr } // © 2015 Hugo Landau ISC License service-2.0.16/service_test.go000066400000000000000000000021641277500134300162730ustar00rootroot00000000000000package service_test import "gopkg.in/hlandau/service.v2" // The following example illustrates the minimal skeleton structure to // implement a daemon. This example can run as a service on Windows or a daemon // on Linux. The systemd notify protocol is supported. func Example() { service.Main(&service.Info{ Title: "Foobar Web Server", Name: "foobar", Description: "Foobar Web Server is the greatest webserver ever.", RunFunc: func(smgr service.Manager) error { // Start up your service. // ... // Once initialization requiring root is done, call this. err := smgr.DropPrivileges() if err != nil { return err } // When it is ready to serve requests, call this. // You must call DropPrivileges first. smgr.SetStarted() // Optionally set a status smgr.SetStatus("foobar: running ok") loop: for { select { // Handle requests, or do so in another goroutine controlled from here. case <-smgr.StopChan(): break loop } } // Do any necessary teardown. // ... return nil }, }) } // © 2015 Hugo Landau ISC License service-2.0.16/service_unix.go000066400000000000000000000110211277500134300162670ustar00rootroot00000000000000// +build !windows package service import ( "fmt" "gopkg.in/hlandau/easyconfig.v1/cflag" "gopkg.in/hlandau/service.v2/daemon" "gopkg.in/hlandau/service.v2/daemon/bansuid" "gopkg.in/hlandau/svcutils.v1/caps" "gopkg.in/hlandau/svcutils.v1/passwd" "gopkg.in/hlandau/svcutils.v1/pidfile" "gopkg.in/hlandau/svcutils.v1/systemd" "os" "strconv" ) // This will always point to a path which the platform guarantees is an empty // directory. You can use it as your default chroot path if your service doesn't // access the filesystem after it's started. // // On Linux, the FHS provides that "/var/empty" is an empty directory, so it // points to that. var EmptyChrootPath = daemon.EmptyChrootPath var ( uidFlag = cflag.String(fg, "uid", "", "UID to run as (default: don't drop privileges)") gidFlag = cflag.String(fg, "gid", "", "GID to run as (default: don't drop privileges)") daemonizeFlag = cflag.Bool(fg, "daemon", false, "Run as daemon? (doesn't fork)") stderrFlag = cflag.Bool(fg, "stderr", false, "Keep stderr open when daemonizing") chrootFlag = cflag.String(fg, "chroot", "", "Chroot to a directory (must set UID, GID) (\"/\" disables)") pidfileFlag = cflag.String(fg, "pidfile", "", "Write PID to file with given filename and hold a write lock") forkFlag = cflag.Bool(fg, "fork", false, "Fork? (implies -daemon)") ) func systemdUpdateStatus(status string) error { return systemd.NotifySend(status) } func (info *Info) serviceMain() error { if forkFlag.Value() { isParent, err := daemon.Fork() if err != nil { return err } if isParent { os.Exit(0) } daemonizeFlag.SetValue(true) } err := daemon.Init() if err != nil { return err } err = systemdUpdateStatus("\n") if err == nil { info.systemd = true } // default: daemon=no, stderr=yes // --daemon: daemon=yes, stderr=no // systemd/--daemon --stderr: daemon=yes, stderr=yes // systemd --daemon: daemon=yes, stderr=no daemonize := daemonizeFlag.Value() keepStderr := stderrFlag.Value() if !daemonize && info.systemd { daemonize = true keepStderr = true } if daemonize { err := daemon.Daemonize(keepStderr) if err != nil { return err } } if pidfileFlag.Value() != "" { info.pidFileName = pidfileFlag.Value() err = info.openPIDFile() if err != nil { return err } defer info.closePIDFile() } return info.runInteractively() } func (info *Info) openPIDFile() error { f, err := pidfile.Open(info.pidFileName) info.pidFile = f return err } func (info *Info) closePIDFile() { if info.pidFile != nil { info.pidFile.Close() } } func (h *ihandler) DropPrivileges() error { if h.dropped { return nil } // Extras if !h.info.NoBanSuid { // Try and bansuid, but don't process errors. It may not be supported on // the current platform, and Linux won't allow SECUREBITS to be set unless // one is root (or has the right capability). This is basically a // best-effort thing. bansuid.BanSuid() } // Various fixups if uidFlag.Value() != "" && gidFlag.Value() == "" { gid, err := passwd.GetGIDForUID(uidFlag.Value()) if err != nil { return err } gidFlag.SetValue(strconv.FormatInt(int64(gid), 10)) } if h.info.DefaultChroot == "" { h.info.DefaultChroot = "/" } chrootPath := chrootFlag.Value() if chrootPath == "" { chrootPath = h.info.DefaultChroot } uid := -1 gid := -1 if uidFlag.Value() != "" { var err error uid, err = passwd.ParseUID(uidFlag.Value()) if err != nil { return err } gid, err = passwd.ParseGID(gidFlag.Value()) if err != nil { return err } } if (uid <= 0) != (gid <= 0) { return fmt.Errorf("Either both or neither of the UID and GID must be positive") } if uid > 0 { chrootErr, err := daemon.DropPrivileges(uid, gid, chrootPath) if err != nil { return fmt.Errorf("Failed to drop privileges: %v", err) } if chrootErr != nil && chrootFlag.Value() != "" && chrootFlag.Value() != "/" { return fmt.Errorf("Failed to chroot: %v", chrootErr) } } else if chrootFlag.Value() != "" && chrootFlag.Value() != "/" { return fmt.Errorf("Must use privilege dropping to use chroot; set -uid") } // If we still have any caps (maybe because we didn't setuid), try and drop them. err := caps.Drop() if err != nil { return fmt.Errorf("cannot drop caps: %v", err) } if !h.info.AllowRoot && daemon.IsRoot() { return fmt.Errorf("Daemon must not run as root or with capabilities; run as non-root user or use -uid") } h.dropped = true return nil } // © 2015 Hugo Landau ISC License service-2.0.16/service_windows.go000066400000000000000000000147221277500134300170110ustar00rootroot00000000000000package service import ( "fmt" "github.com/btcsuite/winsvc/mgr" "github.com/btcsuite/winsvc/svc" "gopkg.in/hlandau/easyconfig.v1/cflag" "gopkg.in/hlandau/svcutils.v1/exepath" "os" "time" ) // This is always empty on Windows, as Windows does not support chrooting. // It is present to allow code relying upon it to compile upon all platforms. var EmptyChrootPath = "" var ( serviceFlag = cflag.String(fg, "do", "", "service command (one of: start, stop, install, remove)") ) var errNotSupported = fmt.Errorf("not supported") func systemdUpdateStatus(status string) error { return errNotSupported } // handler is used when running as a service. // Otherwise we use the generic ihandler. type handler struct { info *Info startedChan chan struct{} stopChan chan struct{} status string dropped bool } func (h *handler) DropPrivileges() error { h.dropped = true return nil } func (h *ihandler) DropPrivileges() error { h.dropped = true return nil } func (h *handler) SetStarted() { if !h.dropped { panic("service must call DropPrivileges before calling SetStarted") } select { case h.startedChan <- struct{}{}: default: } } func (h *handler) StopChan() <-chan struct{} { return h.stopChan } func (h *handler) SetStatus(status string) { h.status = status } func (h *handler) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) { const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown changes <- svc.Status{State: svc.StartPending} h.startedChan = make(chan struct{}, 1) h.stopChan = make(chan struct{}) doneChan := make(chan error) started := false stopping := false go func() { err := h.info.RunFunc(h) doneChan <- err }() var err error loop: for { select { case c := <-r: switch c.Cmd { case svc.Interrogate: changes <- c.CurrentStatus case svc.Stop, svc.Shutdown: // Service stop is pending. Don't accept any more commands while pending. changes <- svc.Status{State: svc.StopPending} if !stopping { stopping = true close(h.stopChan) } default: // Unexpected control request } case <-h.startedChan: if started { panic("must not call SetStarted() more than once") } started = true changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} case err = <-doneChan: break loop } } if err == nil { changes <- svc.Status{State: svc.Stopped} return false, 0 } else { return false, 1 } } func isInteractive() bool { interactive, err := svc.IsAnInteractiveSession() if err != nil { return false } return interactive } func (info *Info) installService() error { svcName := info.Name // Connect to the Windows service manager. serviceManager, err := mgr.Connect() if err != nil { return err } defer serviceManager.Disconnect() // Ensure the service doesn't already exist. service, err := serviceManager.OpenService(svcName) if err == nil { service.Close() return fmt.Errorf("service %s already exists", svcName) } // Install the service. service, err = serviceManager.CreateService(svcName, exepath.Abs, mgr.Config{ DisplayName: info.Title, Description: info.Description, StartType: mgr.StartAutomatic, ErrorControl: mgr.ErrorNormal, }) if err != nil { return err } defer service.Close() // TODO: event log return nil } func (info *Info) removeService() error { svcName := info.Name // Connect to the Windows service manager. serviceManager, err := mgr.Connect() if err != nil { return err } defer serviceManager.Disconnect() // Ensure the service exists. service, err := serviceManager.OpenService(svcName) if err != nil { return fmt.Errorf("service %s is not installed", svcName) } defer service.Close() // Remove the service. err = service.Delete() if err != nil { return err } return nil } func (info *Info) startService() error { svcName := info.Name // Connect to the Windows service manager. serviceManager, err := mgr.Connect() if err != nil { return err } defer serviceManager.Disconnect() service, err := serviceManager.OpenService(svcName) if err != nil { return fmt.Errorf("could not access service: %v", err) } defer service.Close() err = service.Start(os.Args) if err != nil { return fmt.Errorf("could not start service: %v", err) } return nil } func (info *Info) controlService(c svc.Cmd, to svc.State) error { svcName := info.Name // Connect to the Windows service manager. serviceManager, err := mgr.Connect() if err != nil { return err } defer serviceManager.Disconnect() service, err := serviceManager.OpenService(svcName) if err != nil { return fmt.Errorf("could not access service: %v", err) } defer service.Close() // Send the control message. status, err := service.Control(c) if err != nil { return fmt.Errorf("could not send control=%d: %v", c, err) } // Wait. for status.State != to { time.Sleep(300 * time.Millisecond) status, err = service.Query() if err != nil { return fmt.Errorf("could not retrieve service status: %v", err) } } return nil } func (info *Info) stopService() error { return info.controlService(svc.Stop, svc.Stopped) } func (info *Info) runAsService() error { // TODO: event log err := svc.Run(info.Name, &handler{info: info}) if err != nil { return err } return nil } func (info *Info) serviceMain() error { switch serviceFlag.Value() { case "install": return info.installService() case "remove": return info.removeService() case "start": return info.startService() case "stop": return info.stopService() default: // ... } interactive := isInteractive() if !interactive { return info.runAsService() } return info.runInteractively() } // Copyright © 2013-2014 Conformal Systems LLC. // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // // © 2014 Hugo Landau ISC License