├── go.mod ├── gsptcall ├── gsptcall-nocgo.go ├── gsptcall-cgo.go └── gsptcall.go ├── daemon ├── bansuid │ ├── prctl_nlinux.go │ ├── prctl.go │ └── prctl_linux.go ├── daemon.go └── droppriv.go ├── .github ├── workflows │ └── go.yml └── README.md ├── service_test.go ├── service_unix.go ├── service_windows.go └── service.go /go.mod: -------------------------------------------------------------------------------- 1 | module gopkg.in/hlandau/service.v3 2 | 3 | go 1.19 4 | -------------------------------------------------------------------------------- /gsptcall/gsptcall-nocgo.go: -------------------------------------------------------------------------------- 1 | // +build !cgo !unix 2 | 3 | package gsptcall 4 | 5 | func setProcTitle(title string) { 6 | } 7 | -------------------------------------------------------------------------------- /daemon/bansuid/prctl_nlinux.go: -------------------------------------------------------------------------------- 1 | // +build !linux 2 | 3 | package bansuid 4 | 5 | func banSuid() error { 6 | return ErrNotSupported 7 | } 8 | -------------------------------------------------------------------------------- /gsptcall/gsptcall-cgo.go: -------------------------------------------------------------------------------- 1 | // +build cgo,unix 2 | 3 | package gsptcall 4 | 5 | import "github.com/erikdubbelboer/gspt" 6 | 7 | func setProcTitle(title string) { 8 | gspt.SetProcTitle(title) 9 | } 10 | -------------------------------------------------------------------------------- /gsptcall/gsptcall.go: -------------------------------------------------------------------------------- 1 | // Package gsptcall provides a call wrapper for SetProcTitle which does nothing 2 | // when it is not supported. 3 | package gsptcall 4 | 5 | // Calls erikdubbelboer/gspt.SetProcTitle, but only on UNIX platforms and where 6 | // cgo is enabled. Otherwise, it is a no-op. 7 | func SetProcTitle(title string) { 8 | setProcTitle(title) 9 | } 10 | -------------------------------------------------------------------------------- /daemon/bansuid/prctl.go: -------------------------------------------------------------------------------- 1 | // Package bansuid provides a function to prevent processes from reacquiring privileges. 2 | package bansuid 3 | 4 | import "errors" 5 | 6 | // On Linux, uses prctl() SECUREBITS and NO_NEW_PRIVS to prevent the process or its descendants 7 | // from ever obtaining privileges by execing a suid/sgid/cap xattr binary. Returns ErrNotSupported 8 | // if platform is not supported. May return other errors. 9 | func BanSuid() error { 10 | return banSuid() 11 | } 12 | 13 | // Returned by BanSuid if it is not supported on the current platform. 14 | var ErrNotSupported = errors.New("bansuid not supported") 15 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: ["master", "dev"] 5 | pull_request: 6 | branches: ["master"] 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | go-version: ["1.19"] 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Setup Go (${{matrix.go-version}}) 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: ${{matrix.go-version}} 20 | 21 | - name: Install non-Go dependencies 22 | run: sudo apt-get install libcap-dev 23 | 24 | - name: Install Go dependencies 25 | run: go get . 26 | 27 | - name: Build 28 | run: go build -v ./... 29 | 30 | - name: Test 31 | run: go test -v ./... 32 | -------------------------------------------------------------------------------- /service_test.go: -------------------------------------------------------------------------------- 1 | package service_test 2 | 3 | import "gopkg.in/hlandau/service.v3" 4 | 5 | // The following example illustrates the minimal skeleton structure to 6 | // implement a daemon. This example can run as a service on Windows or a daemon 7 | // on Linux. The systemd notify protocol is supported. 8 | func Example() { 9 | service.Main(&service.Info{ 10 | Title: "Foobar Web Server", 11 | Name: "foobar", 12 | Description: "Foobar Web Server is the greatest webserver ever.", 13 | 14 | Config: service.Config{ 15 | Daemon: true, 16 | Stderr: true, 17 | PIDFile: "/run/foobar.pid", 18 | 19 | UID: "nobody", 20 | Chroot: "/var/empty", 21 | }, 22 | 23 | RunFunc: func(smgr service.Manager) error { 24 | // Start up your service. 25 | // ... 26 | 27 | // Once initialization requiring root is done, call this. 28 | err := smgr.DropPrivileges() 29 | if err != nil { 30 | return err 31 | } 32 | 33 | // When it is ready to serve requests, call this. 34 | // You must call DropPrivileges first. 35 | smgr.SetStarted() 36 | 37 | // Optionally set a status 38 | smgr.SetStatus("foobar: running ok") 39 | 40 | loop: 41 | for { 42 | select { 43 | // Handle requests, or do so in another goroutine controlled from here. 44 | case <-smgr.StopChan(): 45 | break loop 46 | } 47 | } 48 | 49 | // Do any necessary teardown. 50 | // ... 51 | 52 | return nil 53 | }, 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /daemon/bansuid/prctl_linux.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | 3 | package bansuid 4 | 5 | import ( 6 | "fmt" 7 | "syscall" 8 | ) 9 | 10 | func banSuid() error { 11 | err := setNoNewPrivs() 12 | if err != nil { 13 | return err 14 | } 15 | 16 | // TODO: Consider use of capability bounding sets. 17 | // Though should be made unnecessary by NO_NEW_PRIVS. 18 | 19 | // Setting SECUREBITS requires capabilities we may not have if we are not running as root, 20 | // so we do this second. 21 | err = setSecurebits() 22 | if err != nil { 23 | return err 24 | } 25 | 26 | return nil 27 | } 28 | 29 | func setNoNewPrivs() error { 30 | err := prctl(pPR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) 31 | if err != nil { 32 | return fmt.Errorf("cannot set NO_NEW_PRIVS: %v", err) 33 | } 34 | 35 | return nil 36 | } 37 | 38 | func setSecurebits() error { 39 | err := prctl(pPR_SET_SECUREBITS, 40 | sSECBIT_NOROOT|sSECBIT_NOROOT_LOCKED|sSECBIT_KEEP_CAPS_LOCKED, 0, 0, 0) 41 | if err != nil { 42 | return fmt.Errorf("cannot set SECUREBITS: %v", err) 43 | } 44 | 45 | return nil 46 | } 47 | 48 | const ( 49 | pPR_SET_SECCOMP = 22 50 | pPR_CAPBSET_DROP = 24 51 | pPR_SET_SECUREBITS = 28 52 | pPR_SET_NO_NEW_PRIVS = 36 53 | 54 | sSECBIT_NOROOT = 1 << 0 55 | sSECBIT_NOROOT_LOCKED = 1 << 1 56 | sSECBIT_NO_SETUID_FIXUP = 1 << 2 57 | sSECBIT_NO_SETUID_FIXUP_LOCKED = 1 << 3 58 | sSECBIT_KEEP_CAPS = 1 << 4 59 | sSECBIT_KEEP_CAPS_LOCKED = 1 << 5 60 | ) 61 | 62 | func prctl(opt int, arg2, arg3, arg4, arg5 uint64) error { 63 | _, _, e1 := syscall.Syscall6(syscall.SYS_PRCTL, uintptr(opt), 64 | uintptr(arg2), uintptr(arg3), uintptr(arg4), uintptr(arg5), 0) 65 | if e1 != 0 { 66 | return e1 67 | } 68 | 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /daemon/daemon.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | // Package daemon provides functions to assist with the writing of UNIX-style 4 | // daemons in go. 5 | package daemon 6 | 7 | import ( 8 | "gopkg.in/hlandau/svcutils.v1/dupfd" 9 | "gopkg.in/hlandau/svcutils.v1/exepath" 10 | "os" 11 | "syscall" 12 | ) 13 | 14 | // Initialises a daemon with recommended values. Called by Daemonize. 15 | // 16 | // Currently, this only calls umask(0) and chdir("/"). 17 | func Init() error { 18 | syscall.Umask(0) 19 | 20 | err := syscall.Chdir("/") 21 | if err != nil { 22 | return err 23 | } 24 | 25 | // setrlimit RLIMIT_CORE 26 | return nil 27 | } 28 | 29 | const forkedArg = "$*_FORKED_*$" 30 | 31 | // Psuedo-forks by re-executing the current binary with a special command line 32 | // argument telling it not to re-execute itself again. Returns true in the 33 | // parent process and false in the child. 34 | func Fork() (isParent bool, err error) { 35 | if os.Args[len(os.Args)-1] == forkedArg { 36 | os.Args = os.Args[0 : len(os.Args)-1] 37 | return false, nil 38 | } 39 | 40 | newArgs := make([]string, 0, len(os.Args)) 41 | newArgs = append(newArgs, exepath.Abs) 42 | newArgs = append(newArgs, os.Args[1:]...) 43 | newArgs = append(newArgs, forkedArg) 44 | 45 | // Start the child process. 46 | // 47 | // Pass along the standard FD for now - we'll remap them to /dev/null 48 | // in due time. This ensures anything expecting these to exist isn't confused, 49 | // and allows pre-daemonization failures to at least get output to somewhere. 50 | proc, err := os.StartProcess(exepath.Abs, newArgs, &os.ProcAttr{ 51 | Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}, 52 | }) 53 | if err != nil { 54 | return true, err 55 | } 56 | 57 | proc.Release() 58 | return true, nil 59 | } 60 | 61 | var haveStderr = true 62 | 63 | // Returns true unless stderr has been closed (remapped to /dev/null) as part 64 | // of daemonization. Can be used to determine whether logging to stderr is 65 | // useful. 66 | func HaveStderr() bool { 67 | return haveStderr 68 | } 69 | 70 | // Daemonizes but doesn't fork. 71 | // 72 | // The stdin, stdout and, unless keepStderr is specified, stderr fds are 73 | // remapped to /dev/null. setsid is called. 74 | // 75 | // The process changes its current directory to /. 76 | // 77 | // If you intend to call DropPrivileges, call it after calling this function, 78 | // as /dev/null will no longer be available after privileges are dropped. 79 | func Daemonize(keepStderr bool) error { 80 | null_f, err := os.OpenFile("/dev/null", os.O_RDWR, 0) 81 | if err != nil { 82 | return err 83 | } 84 | defer null_f.Close() 85 | 86 | stdin_fd := int(os.Stdin.Fd()) 87 | stdout_fd := int(os.Stdout.Fd()) 88 | stderr_fd := int(os.Stderr.Fd()) 89 | 90 | // ... reopen fds 0, 1, 2 as /dev/null ... 91 | // Since dup2 closes fds which are already open we needn't close the above fds. 92 | // This lets us avoid race conditions. 93 | null_fd := int(null_f.Fd()) 94 | err = dupfd.Dup2(null_fd, stdin_fd) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | err = dupfd.Dup2(null_fd, stdout_fd) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | if !keepStderr { 105 | err = dupfd.Dup2(null_fd, stderr_fd) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | haveStderr = false 111 | } 112 | 113 | // This may fail if we're not root 114 | syscall.Setsid() 115 | 116 | // Daemonize implies Init. 117 | return Init() 118 | } 119 | -------------------------------------------------------------------------------- /daemon/droppriv.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package daemon 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "gopkg.in/hlandau/svcutils.v1/caps" 9 | "gopkg.in/hlandau/svcutils.v1/chroot" 10 | "gopkg.in/hlandau/svcutils.v1/passwd" 11 | "gopkg.in/hlandau/svcutils.v1/setuid" 12 | "net" 13 | "runtime" 14 | "sync" 15 | "syscall" 16 | ) 17 | 18 | // Drops privileges to the specified UID and GID. 19 | // This function does nothing and returns no error if all E?[UG]IDs are nonzero. 20 | // 21 | // If chrootDir is not empty, the process is chrooted into it. The directory 22 | // must exist. The function tests that privilege dropping has been successful 23 | // by attempting to setuid(0), which must fail. 24 | // 25 | // The current directory is set to / inside the chroot. 26 | // 27 | // The function ensures that /etc/hosts and /etc/resolv.conf are loaded before 28 | // chrooting, so name service should continue to be available. 29 | func DropPrivileges(UID, GID int, chrootDir string) (chrootErr error, err error) { 30 | // chroot and set UID and GIDs 31 | chrootErr, err = dropPrivileges(UID, GID, chrootDir) 32 | if err != nil { 33 | err = fmt.Errorf("dropPrivileges failed: %v", err) 34 | return 35 | } 36 | 37 | err = syscall.Chdir("/") 38 | if err != nil { 39 | return 40 | } 41 | 42 | err = ensureNoPrivs() 43 | if err != nil { 44 | err = fmt.Errorf("ensure no privs failed: %v", err) 45 | return 46 | } 47 | 48 | return 49 | } 50 | 51 | func dropPrivileges(UID, GID int, chrootDir string) (chrootErr error, err error) { 52 | if (UID <= 0) != (GID <= 0) { 53 | return nil, errors.New("either both or neither UID and GID must be set to positive (i.e. valid, non-root) values") 54 | } 55 | 56 | var gids []int 57 | if UID > 0 { 58 | gids, err = passwd.GetExtraGIDs(GID) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | gids = append(gids, GID) 64 | } 65 | 66 | chrootErr = tryChroot(chrootDir) 67 | 68 | if UID > 0 { 69 | err = tryDropPrivileges(UID, GID, gids) 70 | if err != nil { 71 | return 72 | } 73 | } 74 | 75 | return 76 | } 77 | 78 | var warnOnce sync.Once 79 | 80 | func tryDropPrivileges(UID, GID int, gids []int) error { 81 | if UID <= 0 || GID <= 0 { 82 | return errors.New("invalid UID/GID specified so cannot setuid/setgid") 83 | } 84 | 85 | if runtime.GOOS == "linux" { 86 | ver := runtime.Version() 87 | if ver == "go1.5" || ver == "go1.5.1" { 88 | 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") 89 | } 90 | } 91 | 92 | err := setuid.Setgroups(gids) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | err = setuid.Setresgid(GID, GID, GID) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | err = setuid.Setresuid(UID, UID, UID) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | return nil 108 | } 109 | 110 | func tryChroot(path string) error { 111 | if path == "/" { 112 | path = "" 113 | } 114 | 115 | if path == "" { 116 | return nil 117 | } 118 | 119 | ensureResolverConfigIsLoaded() 120 | 121 | err := chroot.Chroot(path) 122 | if err != nil { 123 | return err 124 | } 125 | 126 | return nil 127 | } 128 | 129 | func ensureResolverConfigIsLoaded() { 130 | c, err := net.Dial("udp", "un_localhost:1") 131 | if err == nil { 132 | c.Close() 133 | } 134 | } 135 | 136 | func ensureNoPrivs() error { 137 | if IsRoot() { 138 | return errors.New("still have non-zero UID or GID or capabilities") 139 | } 140 | 141 | err := setuid.Setuid(0) 142 | if err == nil { 143 | return errors.New("Can't drop privileges - setuid(0) still succeeded") 144 | } 145 | 146 | err = setuid.Setgid(0) 147 | if err == nil { 148 | return errors.New("Can't drop privileges - setgid(0) still succeeded") 149 | } 150 | 151 | return nil 152 | } 153 | 154 | // Returns true if either or both of the following are true: 155 | // 156 | // Any of the UID, EUID, GID or EGID are zero. 157 | // 158 | // On supported platforms which support capabilities (currently Linux), any 159 | // capabilities are present. 160 | func IsRoot() bool { 161 | return caps.HaveAny() || isRoot() 162 | } 163 | 164 | func isRoot() bool { 165 | return syscall.Getuid() == 0 || syscall.Geteuid() == 0 || 166 | syscall.Getgid() == 0 || syscall.Getegid() == 0 167 | } 168 | 169 | // This is set to a path which should be empty on the target platform. 170 | // 171 | // On Linux, the FHS provides that "/var/empty" should always be empty. 172 | var EmptyChrootPath = "/var/empty" 173 | -------------------------------------------------------------------------------- /service_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package service 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "strconv" 10 | 11 | "gopkg.in/hlandau/service.v3/daemon" 12 | "gopkg.in/hlandau/service.v3/daemon/bansuid" 13 | "gopkg.in/hlandau/svcutils.v1/caps" 14 | "gopkg.in/hlandau/svcutils.v1/passwd" 15 | "gopkg.in/hlandau/svcutils.v1/pidfile" 16 | "gopkg.in/hlandau/svcutils.v1/systemd" 17 | ) 18 | 19 | // This will always point to a path which the platform guarantees is an empty 20 | // directory. You can use it as your default chroot path if your service doesn't 21 | // access the filesystem after it's started. 22 | // 23 | // On Linux, the FHS provides that "/var/empty" is an empty directory, so it 24 | // points to that. 25 | var EmptyChrootPath = daemon.EmptyChrootPath 26 | 27 | func usingPlatform(platformName string) bool { 28 | return platformName == "unix" 29 | } 30 | 31 | func systemdUpdateStatus(status string) error { 32 | return systemd.NotifySend(status) 33 | } 34 | 35 | func (info *Info) serviceMain() error { 36 | if info.Config.Fork { 37 | isParent, err := daemon.Fork() 38 | if err != nil { 39 | return err 40 | } 41 | 42 | if isParent { 43 | os.Exit(0) 44 | } 45 | 46 | info.Config.Daemon = true 47 | } 48 | 49 | err := daemon.Init() 50 | if err != nil { 51 | return err 52 | } 53 | 54 | err = systemdUpdateStatus("\n") 55 | if err == nil { 56 | info.systemd = true 57 | } 58 | 59 | // default: daemon=no, stderr=yes 60 | // --daemon: daemon=yes, stderr=no 61 | // systemd/--daemon --stderr: daemon=yes, stderr=yes 62 | // systemd --daemon: daemon=yes, stderr=no 63 | daemonize := info.Config.Daemon 64 | keepStderr := info.Config.Stderr 65 | if !daemonize && info.systemd { 66 | daemonize = true 67 | keepStderr = true 68 | } 69 | 70 | if daemonize { 71 | err := daemon.Daemonize(keepStderr) 72 | if err != nil { 73 | return err 74 | } 75 | } 76 | 77 | if info.Config.PIDFile != "" { 78 | info.pidFileName = info.Config.PIDFile 79 | 80 | err = info.openPIDFile() 81 | if err != nil { 82 | return err 83 | } 84 | 85 | defer info.closePIDFile() 86 | } 87 | 88 | return info.runInteractively() 89 | } 90 | 91 | func (info *Info) openPIDFile() error { 92 | f, err := pidfile.Open(info.pidFileName) 93 | info.pidFile = f 94 | return err 95 | } 96 | 97 | func (info *Info) closePIDFile() { 98 | if info.pidFile != nil { 99 | info.pidFile.Close() 100 | } 101 | } 102 | 103 | func (h *ihandler) DropPrivileges() error { 104 | if h.dropped { 105 | return nil 106 | } 107 | 108 | // Extras 109 | if !h.info.NoBanSuid { 110 | // Try and bansuid, but don't process errors. It may not be supported on 111 | // the current platform, and Linux won't allow SECUREBITS to be set unless 112 | // one is root (or has the right capability). This is basically a 113 | // best-effort thing. 114 | bansuid.BanSuid() 115 | } 116 | 117 | // Various fixups 118 | if h.info.Config.UID != "" && h.info.Config.GID == "" { 119 | gid, err := passwd.GetGIDForUID(h.info.Config.UID) 120 | if err != nil { 121 | return err 122 | } 123 | h.info.Config.GID = strconv.FormatInt(int64(gid), 10) 124 | } 125 | 126 | if h.info.DefaultChroot == "" { 127 | h.info.DefaultChroot = "/" 128 | } 129 | 130 | chrootPath := h.info.Config.Chroot 131 | if chrootPath == "" { 132 | chrootPath = h.info.DefaultChroot 133 | } 134 | 135 | uid := -1 136 | gid := -1 137 | if h.info.Config.UID != "" { 138 | var err error 139 | uid, err = passwd.ParseUID(h.info.Config.UID) 140 | if err != nil { 141 | return err 142 | } 143 | 144 | gid, err = passwd.ParseGID(h.info.Config.GID) 145 | if err != nil { 146 | return err 147 | } 148 | } 149 | 150 | if (uid <= 0) != (gid <= 0) { 151 | return fmt.Errorf("Either both or neither of the UID and GID must be positive") 152 | } 153 | 154 | if uid > 0 { 155 | chrootErr, err := daemon.DropPrivileges(uid, gid, chrootPath) 156 | if err != nil { 157 | return fmt.Errorf("Failed to drop privileges: %v", err) 158 | } 159 | if chrootErr != nil && h.info.Config.Chroot != "" && h.info.Config.Chroot != "/" { 160 | return fmt.Errorf("Failed to chroot: %v", chrootErr) 161 | } 162 | } else if h.info.Config.Chroot != "" && h.info.Config.Chroot != "/" { 163 | return fmt.Errorf("Must use privilege dropping to use chroot; set -uid") 164 | } 165 | 166 | // If we still have any caps (maybe because we didn't setuid), try and drop them. 167 | err := caps.Drop() 168 | if err != nil { 169 | return fmt.Errorf("cannot drop caps: %v", err) 170 | } 171 | 172 | if !h.info.AllowRoot && daemon.IsRoot() { 173 | return fmt.Errorf("Daemon must not run as root or with capabilities; run as non-root user or use -uid") 174 | } 175 | 176 | h.dropped = true 177 | return nil 178 | } 179 | -------------------------------------------------------------------------------- /service_windows.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "golang.org/x/sys/windows/svc" 9 | "golang.org/x/sys/windows/svc/mgr" 10 | "gopkg.in/hlandau/svcutils.v1/exepath" 11 | ) 12 | 13 | // This is always empty on Windows, as Windows does not support chrooting. 14 | // It is present to allow code relying upon it to compile upon all platforms. 15 | var EmptyChrootPath = "" 16 | 17 | var errNotSupported = fmt.Errorf("not supported") 18 | 19 | func systemdUpdateStatus(status string) error { 20 | return errNotSupported 21 | } 22 | 23 | func usingPlatform(platformName string) bool { 24 | return platformName == "windows" 25 | } 26 | 27 | // handler is used when running as a service. 28 | // Otherwise we use the generic ihandler. 29 | type handler struct { 30 | info *Info 31 | startedChan chan struct{} 32 | stopChan chan struct{} 33 | status string 34 | dropped bool 35 | } 36 | 37 | func (h *handler) DropPrivileges() error { 38 | h.dropped = true 39 | return nil 40 | } 41 | 42 | func (h *ihandler) DropPrivileges() error { 43 | h.dropped = true 44 | return nil 45 | } 46 | 47 | func (h *handler) SetStarted() { 48 | if !h.dropped { 49 | panic("service must call DropPrivileges before calling SetStarted") 50 | } 51 | 52 | select { 53 | case h.startedChan <- struct{}{}: 54 | default: 55 | } 56 | } 57 | 58 | func (h *handler) StopChan() <-chan struct{} { 59 | return h.stopChan 60 | } 61 | 62 | func (h *handler) SetStatus(status string) { 63 | h.status = status 64 | } 65 | 66 | func (h *handler) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) { 67 | const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown 68 | changes <- svc.Status{State: svc.StartPending} 69 | 70 | h.startedChan = make(chan struct{}, 1) 71 | h.stopChan = make(chan struct{}) 72 | doneChan := make(chan error) 73 | started := false 74 | stopping := false 75 | 76 | go func() { 77 | err := h.info.RunFunc(h) 78 | doneChan <- err 79 | }() 80 | 81 | var err error 82 | 83 | loop: 84 | for { 85 | select { 86 | case c := <-r: 87 | switch c.Cmd { 88 | case svc.Interrogate: 89 | changes <- c.CurrentStatus 90 | 91 | case svc.Stop, svc.Shutdown: 92 | // Service stop is pending. Don't accept any more commands while pending. 93 | changes <- svc.Status{State: svc.StopPending} 94 | if !stopping { 95 | stopping = true 96 | close(h.stopChan) 97 | } 98 | 99 | default: 100 | // Unexpected control request 101 | } 102 | 103 | case <-h.startedChan: 104 | if started { 105 | panic("must not call SetStarted() more than once") 106 | } 107 | started = true 108 | changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} 109 | 110 | case err = <-doneChan: 111 | break loop 112 | } 113 | } 114 | 115 | if err == nil { 116 | changes <- svc.Status{State: svc.Stopped} 117 | return false, 0 118 | } else { 119 | return false, 1 120 | } 121 | } 122 | 123 | func isInteractive() bool { 124 | interactive, err := svc.IsAnInteractiveSession() 125 | if err != nil { 126 | return false 127 | } 128 | return interactive 129 | } 130 | 131 | func (info *Info) installService() error { 132 | svcName := info.Name 133 | 134 | // Connect to the Windows service manager. 135 | serviceManager, err := mgr.Connect() 136 | if err != nil { 137 | return err 138 | } 139 | 140 | defer serviceManager.Disconnect() 141 | 142 | // Ensure the service doesn't already exist. 143 | service, err := serviceManager.OpenService(svcName) 144 | if err == nil { 145 | service.Close() 146 | return fmt.Errorf("service %s already exists", svcName) 147 | } 148 | 149 | // Install the service. 150 | service, err = serviceManager.CreateService(svcName, exepath.Abs, mgr.Config{ 151 | DisplayName: info.Title, 152 | Description: info.Description, 153 | StartType: mgr.StartAutomatic, 154 | ErrorControl: mgr.ErrorNormal, 155 | }) 156 | if err != nil { 157 | return err 158 | } 159 | defer service.Close() 160 | 161 | // TODO: event log 162 | 163 | return nil 164 | } 165 | 166 | func (info *Info) removeService() error { 167 | svcName := info.Name 168 | 169 | // Connect to the Windows service manager. 170 | serviceManager, err := mgr.Connect() 171 | if err != nil { 172 | return err 173 | } 174 | defer serviceManager.Disconnect() 175 | 176 | // Ensure the service exists. 177 | service, err := serviceManager.OpenService(svcName) 178 | if err != nil { 179 | return fmt.Errorf("service %s is not installed", svcName) 180 | } 181 | defer service.Close() 182 | 183 | // Remove the service. 184 | err = service.Delete() 185 | if err != nil { 186 | return err 187 | } 188 | 189 | return nil 190 | } 191 | 192 | func (info *Info) startService() error { 193 | svcName := info.Name 194 | 195 | // Connect to the Windows service manager. 196 | serviceManager, err := mgr.Connect() 197 | if err != nil { 198 | return err 199 | } 200 | defer serviceManager.Disconnect() 201 | 202 | service, err := serviceManager.OpenService(svcName) 203 | if err != nil { 204 | return fmt.Errorf("could not access service: %v", err) 205 | } 206 | defer service.Close() 207 | 208 | err = service.Start(os.Args...) 209 | if err != nil { 210 | return fmt.Errorf("could not start service: %v", err) 211 | } 212 | 213 | return nil 214 | } 215 | 216 | func (info *Info) controlService(c svc.Cmd, to svc.State) error { 217 | svcName := info.Name 218 | 219 | // Connect to the Windows service manager. 220 | serviceManager, err := mgr.Connect() 221 | if err != nil { 222 | return err 223 | } 224 | defer serviceManager.Disconnect() 225 | 226 | service, err := serviceManager.OpenService(svcName) 227 | if err != nil { 228 | return fmt.Errorf("could not access service: %v", err) 229 | } 230 | defer service.Close() 231 | 232 | // Send the control message. 233 | status, err := service.Control(c) 234 | if err != nil { 235 | return fmt.Errorf("could not send control=%d: %v", c, err) 236 | } 237 | 238 | // Wait. 239 | for status.State != to { 240 | time.Sleep(300 * time.Millisecond) 241 | status, err = service.Query() 242 | if err != nil { 243 | return fmt.Errorf("could not retrieve service status: %v", err) 244 | } 245 | } 246 | 247 | return nil 248 | } 249 | 250 | func (info *Info) stopService() error { 251 | return info.controlService(svc.Stop, svc.Stopped) 252 | } 253 | 254 | func (info *Info) runAsService() error { 255 | // TODO: event log 256 | 257 | err := svc.Run(info.Name, &handler{info: info}) 258 | if err != nil { 259 | return err 260 | } 261 | 262 | return nil 263 | } 264 | 265 | func (info *Info) serviceMain() error { 266 | switch info.Config.Command { 267 | case "install": 268 | return info.installService() 269 | case "remove": 270 | return info.removeService() 271 | case "start": 272 | return info.startService() 273 | case "stop": 274 | return info.stopService() 275 | default: 276 | // ... 277 | } 278 | 279 | interactive := isInteractive() 280 | if !interactive { 281 | return info.runAsService() 282 | } 283 | 284 | return info.runInteractively() 285 | } 286 | 287 | // Copyright © 2013-2014 Conformal Systems LLC. 288 | // 289 | // Permission to use, copy, modify, and distribute this software for any 290 | // purpose with or without fee is hereby granted, provided that the above 291 | // copyright notice and this permission notice appear in all copies. 292 | // 293 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 294 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 295 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 296 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 297 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 298 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 299 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 300 | -------------------------------------------------------------------------------- /.github/README.md: -------------------------------------------------------------------------------- 1 | service: Write daemons in Go 2 | ============================ 3 | 4 | [![godocs.io](https://godocs.io/gopkg.in/hlandau/service.v3?status.svg)](https://godocs.io/gopkg.in/hlandau/service.v3) [![Build status](https://github.com/hlandau/service/actions/workflows/go.yml/badge.svg)](#) 5 | 6 | This package enables you to easily write services in Go such that the following concerns are taken care of automatically: 7 | 8 | - Daemonization 9 | - Fork emulation (not recommended, though) 10 | - PID file creation 11 | - Privilege dropping 12 | - Chrooting 13 | - Status notification (supports setproctitle and systemd notify protocol; this support is Go-native and does not introduce any dependency on any systemd library) 14 | - Operation as a Windows service 15 | - Orderly shutdown 16 | 17 | Standard Interface 18 | ------------------ 19 | 20 | Here's a usage example: 21 | 22 | ```go 23 | package main 24 | 25 | import "gopkg.in/hlandau/service.v3" 26 | 27 | func main() { 28 | service.Main(&service.Info{ 29 | Title: "Foobar Web Server", 30 | Name: "foobar", 31 | Description: "Foobar Web Server is the greatest webserver ever.", 32 | 33 | Config: service.Config { 34 | Daemon: true, 35 | Stderr: true, 36 | PIDFile: "/run/foobar.pid", 37 | 38 | UID: "nobody", 39 | Chroot: "/var/empty", 40 | }, 41 | 42 | RunFunc: func(smgr service.Manager) error { 43 | // Start up your service. 44 | // ... 45 | 46 | // Once initialization requiring root is done, call this. 47 | err := smgr.DropPrivileges() 48 | if err != nil { 49 | return err 50 | } 51 | 52 | // When it is ready to serve requests, call this. 53 | // You must call DropPrivileges first. 54 | smgr.SetStarted() 55 | 56 | // Optionally set a status. 57 | smgr.SetStatus("foobar: running ok") 58 | 59 | // Wait until stop is requested. 60 | <-smgr.StopChan() 61 | 62 | // Do any necessary teardown. 63 | // ... 64 | 65 | // Done. 66 | return nil 67 | }, 68 | }) 69 | } 70 | ``` 71 | 72 | You should import the package as "gopkg.in/hlandau/service.v3". Compatibility will be preserved. (Please note that this compatibility guarantee does not extend to subpackages.) 73 | 74 | Simplified Interface 75 | -------------------- 76 | 77 | If you implement the following interface, you can use the simplified interface. This example also demonstrates how to use easyconfig to handle your configuration. 78 | 79 | ```go 80 | func() (Runnable, error) 81 | 82 | type Runnable interface { 83 | Start() error 84 | Stop() error 85 | } 86 | ``` 87 | 88 | Usage example: 89 | 90 | ```go 91 | package main 92 | 93 | import "gopkg.in/hlandau/service.v3" 94 | 95 | type Config struct{} 96 | 97 | // Server which doesn't do anything 98 | type Server struct{} 99 | 100 | func New(cfg Config) (*Server, error) { 101 | // Instantiate the service and bind to ports here 102 | return &Server{}, nil 103 | } 104 | 105 | func (*Server) Start() error { 106 | // Start handling of requests here (must return) 107 | return nil 108 | } 109 | 110 | func (*Server) Stop() error { 111 | // Stop the service here 112 | return nil 113 | } 114 | 115 | func main() { 116 | cfg := Config{} 117 | 118 | /* application: parse config into cfg */ 119 | 120 | service.Main(&service.Info{ 121 | Name: "foobar", 122 | 123 | Config: service.Config { 124 | Daemon: true, 125 | Stderr: true, 126 | PIDFile: "/run/foobar.pid", 127 | 128 | UID: "nobody", 129 | Chroot: "/var/empty", 130 | }, 131 | 132 | NewFunc: func() (service.Runnable, error) { 133 | return New(cfg) 134 | }, 135 | }) 136 | } 137 | ``` 138 | 139 | Changes since v1 140 | ---------------- 141 | 142 | v1 used the "flag" package to register service configuration options like UID, GID, etc. 143 | 144 | v2 uses the "[configurable](https://github.com/hlandau/configurable)" package 145 | to register service configuration options. "configurable" is a neutral 146 | [integration nexus](http://www.devever.net/~hl/nexuses), so it increases the 147 | generality of `service`. However, bear in mind that you are responsible for 148 | ensuring that configuration is loaded before calling service.Main. 149 | 150 | v3 no longer uses [configurable](https://github.com/hlandau/configurable) and 151 | instead uses an explicit configuration model in which service configuration 152 | parameters must be passed explicitly. This reduces dependency closure size. See 153 | package documentation for details. 154 | 155 | v3 removes support for launching a debug HTTP server. An application can 156 | provide this functionality itself if needed. This reduces dependency closure 157 | size by alliowing this package to no longer depend on net/http. 158 | 159 | Using as a Windows service 160 | -------------------------- 161 | 162 | You can use the `Config.Command` field to install and remove the service as a 163 | Windows service. Please note that: 164 | 165 | - You will need to run these commands from an elevated command prompt 166 | (right click on 'Command Prompt' and select 'Run as administrator'). 167 | 168 | - The absolute path of the executable in its current location will be used 169 | as the path to the service. 170 | 171 | - You may need to tweak the command line arguments for the service 172 | to your liking using `services.msc` after installation. 173 | 174 | - You may also use any other method that you like to install or remove 175 | services. No particular command line flag is required; the service will 176 | detect when it is being run as a Windows service automatically. 177 | 178 | ### Manifests 179 | 180 | 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. 181 | 182 | Here is an example manifest: 183 | 184 | ```xml 185 | 186 | 187 | 188 | 189 | 190 | 193 | 194 | 195 | 196 | 197 | ``` 198 | 199 | 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. 200 | 201 | For more information on manifests, see MSDN. 202 | 203 | Use with systemd 204 | ---------------- 205 | 206 | Here is an example systemd unit file with privilege dropping and auto-restart, 207 | assuming that your application forwards command line flags of the form 208 | `.service.foo=bar` as corresponding fields in the `Config` structure: 209 | 210 | [Unit] 211 | Description=short description of the daemon 212 | ;; Optionally make the service dependent on other services 213 | ;Requires=other.service 214 | 215 | [Service] 216 | Type=notify 217 | ExecStart=/path/to/foobar/foobard -service.uid=foobar -service.gid=foobar -service.daemon=1 218 | Restart=always 219 | RestartSec=30 220 | 221 | [Install] 222 | WantedBy=multi-user.target 223 | 224 | Bugs 225 | ---- 226 | 227 | - Testing would be nice, but a library of this nature isn't too susceptible 228 | to unit testing. Something to think about. 229 | 230 | - **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.) 231 | 232 | Platform Support 233 | ---------------- 234 | 235 | The package should work on Windows or any UNIX-like platform, but has been 236 | tested only on the following platforms: 237 | 238 | - Linux 239 | - FreeBSD 240 | - Darwin/OS X 241 | - Windows 242 | 243 | 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. 244 | 245 | Reduced Functionality Mode 246 | -------------------------- 247 | 248 | When built without cgo, the following limitations are imposed: 249 | 250 | - Privilege dropping is not supported at all on Linux. 251 | - UIDs and GIDs must be specified numerically, not as names. 252 | - No supplementary GIDs are configured when dropping privileges (the empty set is configured). 253 | - setproctitle is not supported; status setting is a no-op. 254 | 255 | Utility Library 256 | --------------- 257 | 258 | This package provides a simplified interface built on some functionality 259 | exposed in [hlandau/svcutils](https://github.com/hlandau/svcutils). People who 260 | want something less “magic” may find functions there useful. 261 | 262 | Some functions in that repository may still be useful to people using this 263 | package. For example, the chroot package allows you to (try to) relativize a 264 | path to a chroot, allowing you to address files by their absolute path after 265 | chrooting. 266 | 267 | Licence 268 | ------- 269 | 270 | ISC License 271 | 272 | Permission to use, copy, modify, and distribute this software for any 273 | purpose with or without fee is hereby granted, provided that the above 274 | copyright notice and this permission notice appear in all copies. 275 | 276 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 277 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 278 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 279 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 280 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 281 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 282 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 283 | 284 | -------------------------------------------------------------------------------- /service.go: -------------------------------------------------------------------------------- 1 | // Package service wraps all the complexity of writing daemons while enabling 2 | // seamless integration with OS service management facilities. 3 | // 4 | // # Changes in v3 5 | // 6 | // v2 of this package used [configurable] and [easyconfig] to configure 7 | // service-related parameters. This allowed other packages to automatically 8 | // discover global configurable dials for any package linked into a Go binary 9 | // and expose them as command line arguments. This approach has been deprecated 10 | // as it exposes internal details of an application's organisation as part of its 11 | // CLI interface and makes it hard to maintain over time. This approach is 12 | // deprecated in favour of an explicit approach where service variables are 13 | // specified in a structure, [Config]. 14 | // 15 | // v3 no longer links to [easyconfig], reducing its dependency closure size, 16 | // and instead simply accepts a mandatory Config structure which can be used to 17 | // specify the configuration parameters for a service. 18 | // 19 | // v3 removes support for launching a debug HTTP server. An application can provide 20 | // this functionality itself if needed. This reduces dependency closure size by allowing 21 | // this package to no longer depend on net/http. 22 | // 23 | // # Platform-Specific Configuration Variables 24 | // 25 | // Some fields in [Config] are platform-specific. The fields are present on all 26 | // platforms as Go provides no simple way to omit fields in structure 27 | // definitions on certain platforms. The "platform" annotation on a field 28 | // denotes if a field is platform-specific. If this annotation is omitted, the 29 | // field is supported on all platforms. You can pass the "platform" annotation 30 | // to [UsingPlatform] to determine if a field is currently applicable. 31 | // 32 | // [configurable]: https://github.com/hlandau/configurable 33 | // [easyconfig]: https://github.com/hlandau/easyconfig 34 | package service // import "gopkg.in/hlandau/service.v3" 35 | 36 | import ( 37 | "expvar" 38 | "fmt" 39 | "io" 40 | "os" 41 | "os/signal" 42 | "runtime/pprof" 43 | "sync" 44 | "syscall" 45 | "time" 46 | 47 | "gopkg.in/hlandau/service.v3/gsptcall" 48 | "gopkg.in/hlandau/svcutils.v1/exepath" 49 | ) 50 | 51 | type nullWriter struct{} 52 | 53 | func (nw nullWriter) Write(p []byte) (n int, err error) { 54 | return len(p), nil 55 | } 56 | 57 | func init() { 58 | expvar.NewString("service.startTime").Set(time.Now().String()) 59 | } 60 | 61 | // This function should typically be called directly from func main(). It takes 62 | // care of all housekeeping for running services and handles service lifecycle. 63 | func Main(info *Info) { 64 | info.main() 65 | } 66 | 67 | // The interface between the service library and the application-specific code. 68 | // The application calls the methods in the provided instance of this interface 69 | // at various stages in its lifecycle. 70 | type Manager interface { 71 | // Must be called when the service is ready to drop privileges. 72 | // This must be called before SetStarted(). 73 | DropPrivileges() error 74 | 75 | // Must be called by a service payload when it has finished starting. 76 | SetStarted() 77 | 78 | // A service payload must stop when this channel is closed. 79 | StopChan() <-chan struct{} 80 | 81 | // Called by a service payload to provide a single line of information on the 82 | // current status of that service. 83 | SetStatus(status string) 84 | } 85 | 86 | // Used only by the NewFunc interface. 87 | type Runnable interface { 88 | // Start the runnable. Any initialization requiring root privileges must 89 | // already have been obtained as this will be called after dropping 90 | // privileges. Must return. 91 | Start() error 92 | 93 | // Stop the runnable. Must return. 94 | Stop() error 95 | } 96 | 97 | // An upgrade interface for Runnable, implementation of which is optional. 98 | type StatusSource interface { 99 | // Return a channel on which status messages will be sent. If a Runnable 100 | // implements this, it is guaranteed that the channel will be consumed until 101 | // Stop is called. 102 | StatusChan() <-chan string 103 | } 104 | 105 | // Configuration variables which control how a service is run. 106 | type Config struct { 107 | // If this is non-empty, CPU profiling is initiated on startup and the 108 | // profile is written to the given file. 109 | CPUProfile string `help:"Write CPU profile to file"` 110 | 111 | // UNIX: If this is non-empty, privilege dropping is enabled. The value can be a UID or username. 112 | UID string `help:"UID to run as (default: don't drop privileges)" platform:"unix"` 113 | 114 | // UNIX: If this is non-empty, it is the GID or group name used when dropping 115 | // privileges. If privilege dropping is enabled (UID is non-empty) and this 116 | // is empty, the GID for the given UID is looked up from the system. 117 | GID string `help:"GID to run as (default: don't drop privileges)" platform:"unix"` 118 | 119 | // UNIX: Runs the service as a daemon (aside from forking). This sets up the 120 | // CWD, umask, calls setsid() and remaps stdin and stdout (and stderr, if 121 | // Stderr is not set) to /dev/null. 122 | Daemon bool `help:"Run as daemon? (doesn't fork)" platform:"unix"` 123 | 124 | // UNIX: Fork. Implies Daemon. 125 | Fork bool `help:"Fork? (implies daemon)" platform:"unix"` 126 | 127 | // UNIX: If non-empty, path to a file to write the process PID to. 128 | PIDFile string `help:"Write PID to file with given filename and hold a write lock" platform:"unix"` 129 | 130 | // UNIX: If not "/", the directory to chroot into. Only used if dropping 131 | // privileges (i.e., if UID is non-empty). 132 | Chroot string `help:"Chroot to a directory (must set UID, GID) ('/' disables)" platform:"unix"` 133 | 134 | // UNIX: Keep stderr open if Daemon is set and do not remap it to /dev/null. 135 | Stderr bool `help:"Keep stderr open when daemonizing" platform:"unix"` 136 | 137 | // Windows: Service control command. Can be used to install or uninstall a 138 | // service, or start or stop it. If empty, run the service normally. 139 | // The package automatically detects if it is running under the service manager 140 | // or as a normal process. 141 | Command string `help:"Service command (install, uninstall, start, stop)" platform:"windows"` 142 | } 143 | 144 | // Returns true if a given platform name (e.g. "", "unix", "windows") is currently applicable. 145 | func UsingPlatform(platformName string) bool { 146 | if platformName == "" { 147 | return true 148 | } 149 | return usingPlatform(platformName) 150 | } 151 | 152 | // An instantiable service. 153 | type Info struct { 154 | // Recommended. Codename for the service, e.g. "foobar" 155 | // 156 | // If this is not set, exepath.ProgramName is used, which by default is the 157 | // program's binary basename (e.g. "FooBar.exe" would become "foobar"). 158 | Name string 159 | 160 | // Required unless NewFunc is specified instead. Starts the service. Must not 161 | // return until the service has stopped. Must call smgr.SetStarted() to 162 | // indicate when it has finished starting and use smgr.StopChan() to 163 | // determine when to stop. 164 | // 165 | // Should call SetStatus() periodically with a status string. 166 | RunFunc func(smgr Manager) error 167 | 168 | // Optional. An alternative to RunFunc. If this is provided, RunFunc must not 169 | // be specified, and this package will provide its own implementation of 170 | // RunFunc. 171 | // 172 | // The NewFunc will be called to instantiate the runnable service. 173 | // Privileges will then be dropped and Start will be called. Start must 174 | // return. When the service is to be stopped, Stop will be called. Stop must 175 | // return. 176 | // 177 | // To implement status notification, implement also the StatusSource interface. 178 | NewFunc func() (Runnable, error) 179 | 180 | Title string // Optional. Friendly name for the service, e.g. "Foobar Web Server" 181 | Description string // Optional. Single line description for the service 182 | 183 | AllowRoot bool // May the service run as root? If false, the service will refuse to run as root unless privilege dropping is set. 184 | DefaultChroot string // Default path to chroot to. Use this if the service can be chrooted without consequence. 185 | NoBanSuid bool // Set to true if the ability to execute suid binaries must be retained. 186 | 187 | // This must contain the configuration variables to be used to run the service. It will generally be parsed by an application from a command line. 188 | Config Config 189 | 190 | // Are we being started by systemd with [Service] Type=notify? 191 | // If so, we can issue service status notifications to systemd. 192 | systemd bool 193 | 194 | // Path to created PID file. 195 | pidFileName string 196 | pidFile io.Closer 197 | } 198 | 199 | func (info *Info) main() { 200 | err := info.maine() 201 | if err != nil { 202 | fmt.Fprintf(os.Stderr, "Error in service: %+v\n", err) 203 | os.Exit(1) 204 | } 205 | } 206 | 207 | func (info *Info) maine() error { 208 | if info.Name == "" { 209 | info.Name = exepath.ProgramName 210 | } else if exepath.ProgramNameSetter == "default" { 211 | exepath.ProgramName = info.Name 212 | exepath.ProgramNameSetter = "service" 213 | } 214 | 215 | if info.Name == "" { 216 | panic("service name must be specified") 217 | } 218 | if info.Title == "" { 219 | info.Title = info.Name 220 | } 221 | if info.Description == "" { 222 | info.Description = info.Title 223 | } 224 | 225 | err := info.commonPre() 226 | if err != nil { 227 | return err 228 | } 229 | 230 | err = info.setRunFunc() 231 | if err != nil { 232 | return err 233 | } 234 | 235 | // profiling 236 | if info.Config.CPUProfile != "" { 237 | f, err := os.Create(info.Config.CPUProfile) 238 | if err != nil { 239 | return err 240 | } 241 | pprof.StartCPUProfile(f) 242 | defer f.Close() 243 | defer pprof.StopCPUProfile() 244 | } 245 | 246 | err = info.serviceMain() 247 | 248 | return err 249 | } 250 | 251 | func (info *Info) commonPre() error { 252 | return nil 253 | } 254 | 255 | func (info *Info) setRunFunc() error { 256 | if info.RunFunc != nil { 257 | return nil 258 | } 259 | 260 | if info.NewFunc == nil { 261 | panic("either RunFunc or NewFunc must be specified") 262 | } 263 | 264 | info.RunFunc = func(smgr Manager) error { 265 | // instantiate runnable 266 | r, err := info.NewFunc() 267 | if err != nil { 268 | return err 269 | } 270 | 271 | // setup status channel 272 | getStatusChan := func() <-chan string { 273 | return nil 274 | } 275 | if ss, ok := r.(StatusSource); ok { 276 | getStatusChan = func() <-chan string { 277 | return ss.StatusChan() 278 | } 279 | } 280 | 281 | // drop privileges 282 | err = smgr.DropPrivileges() 283 | if err != nil { 284 | return err 285 | } 286 | 287 | // start 288 | err = r.Start() 289 | if err != nil { 290 | return err 291 | } 292 | 293 | // 294 | smgr.SetStarted() 295 | smgr.SetStatus(info.Name + ": running ok") 296 | 297 | // wait for status messages or stop requests 298 | loop: 299 | for { 300 | select { 301 | case statusMsg := <-getStatusChan(): 302 | smgr.SetStatus(info.Name + ": " + statusMsg) 303 | 304 | case <-smgr.StopChan(): 305 | break loop 306 | } 307 | } 308 | 309 | // stop 310 | return r.Stop() 311 | } 312 | 313 | return nil 314 | } 315 | 316 | type ihandler struct { 317 | info *Info 318 | stopChan chan struct{} 319 | statusMutex sync.Mutex 320 | statusNotifyChan chan struct{} 321 | startedChan chan struct{} 322 | status string 323 | started bool 324 | stopping bool 325 | dropped bool 326 | } 327 | 328 | func (h *ihandler) SetStarted() { 329 | if !h.dropped { 330 | panic("service must call DropPrivileges before calling SetStarted") 331 | } 332 | 333 | select { 334 | case h.startedChan <- struct{}{}: 335 | default: 336 | } 337 | } 338 | 339 | func (h *ihandler) StopChan() <-chan struct{} { 340 | return h.stopChan 341 | } 342 | 343 | func (h *ihandler) SetStatus(status string) { 344 | h.statusMutex.Lock() 345 | h.status = status 346 | h.statusMutex.Unlock() 347 | 348 | select { 349 | case <-h.statusNotifyChan: 350 | default: 351 | } 352 | } 353 | 354 | func (h *ihandler) updateStatus() { 355 | // systemd 356 | if h.info.systemd { 357 | s := "" 358 | if h.started { 359 | s += "READY=1\n" 360 | } 361 | if h.status != "" { 362 | s += "STATUS=" + h.status + "\n" 363 | } 364 | systemdUpdateStatus(s) 365 | // ignore error 366 | } 367 | 368 | if h.status != "" { 369 | gsptcall.SetProcTitle(h.status) 370 | } 371 | } 372 | 373 | func (info *Info) runInteractively() error { 374 | smgr := ihandler{ 375 | info: info, 376 | stopChan: make(chan struct{}), 377 | statusNotifyChan: make(chan struct{}, 1), 378 | startedChan: make(chan struct{}, 1), 379 | } 380 | 381 | doneChan := make(chan error) 382 | go func() { 383 | err := info.RunFunc(&smgr) 384 | doneChan <- err 385 | }() 386 | 387 | sig := make(chan os.Signal, 1) 388 | signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) 389 | 390 | var exitErr error 391 | 392 | loop: 393 | for { 394 | select { 395 | case <-sig: 396 | if !smgr.stopping { 397 | smgr.stopping = true 398 | close(smgr.stopChan) 399 | smgr.updateStatus() 400 | } 401 | case <-smgr.startedChan: 402 | if !smgr.started { 403 | smgr.started = true 404 | smgr.updateStatus() 405 | } 406 | case <-smgr.statusNotifyChan: 407 | smgr.updateStatus() 408 | case exitErr = <-doneChan: 409 | break loop 410 | } 411 | } 412 | 413 | return exitErr 414 | } 415 | --------------------------------------------------------------------------------