mirror of https://github.com/mindoc-org/mindoc.git
添加依赖包
parent
8cc9c6249a
commit
e5b6902fe3
|
@ -0,0 +1,27 @@
|
|||
Copyright (c) 2012 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,21 @@
|
|||
### Extensions to the "os" package.
|
||||
|
||||
[![GoDoc](https://godoc.org/github.com/kardianos/osext?status.svg)](https://godoc.org/github.com/kardianos/osext)
|
||||
|
||||
## Find the current Executable and ExecutableFolder.
|
||||
|
||||
As of go1.8 the Executable function may be found in `os`. The Executable function
|
||||
in the std lib `os` package is used if available.
|
||||
|
||||
There is sometimes utility in finding the current executable file
|
||||
that is running. This can be used for upgrading the current executable
|
||||
or finding resources located relative to the executable file. Both
|
||||
working directory and the os.Args[0] value are arbitrary and cannot
|
||||
be relied on; os.Args[0] can be "faked".
|
||||
|
||||
Multi-platform and supports:
|
||||
* Linux
|
||||
* OS X
|
||||
* Windows
|
||||
* Plan 9
|
||||
* BSDs.
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Extensions to the standard "os" package.
|
||||
package osext // import "github.com/kardianos/osext"
|
||||
|
||||
import "path/filepath"
|
||||
|
||||
var cx, ce = executableClean()
|
||||
|
||||
func executableClean() (string, error) {
|
||||
p, err := executable()
|
||||
return filepath.Clean(p), err
|
||||
}
|
||||
|
||||
// Executable returns an absolute path that can be used to
|
||||
// re-invoke the current program.
|
||||
// It may not be valid after the current program exits.
|
||||
func Executable() (string, error) {
|
||||
return cx, ce
|
||||
}
|
||||
|
||||
// Returns same path as Executable, returns just the folder
|
||||
// path. Excludes the executable name and any trailing slash.
|
||||
func ExecutableFolder() (string, error) {
|
||||
p, err := Executable()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return filepath.Dir(p), nil
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
//+build go1.8,!openbsd
|
||||
|
||||
package osext
|
||||
|
||||
import "os"
|
||||
|
||||
func executable() (string, error) {
|
||||
return os.Executable()
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//+build !go1.8
|
||||
|
||||
package osext
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func executable() (string, error) {
|
||||
f, err := os.Open("/proc/" + strconv.Itoa(os.Getpid()) + "/text")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
return syscall.Fd2path(int(f.Fd()))
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !go1.8,android !go1.8,linux !go1.8,netbsd !go1.8,solaris !go1.8,dragonfly
|
||||
|
||||
package osext
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func executable() (string, error) {
|
||||
switch runtime.GOOS {
|
||||
case "linux", "android":
|
||||
const deletedTag = " (deleted)"
|
||||
execpath, err := os.Readlink("/proc/self/exe")
|
||||
if err != nil {
|
||||
return execpath, err
|
||||
}
|
||||
execpath = strings.TrimSuffix(execpath, deletedTag)
|
||||
execpath = strings.TrimPrefix(execpath, deletedTag)
|
||||
return execpath, nil
|
||||
case "netbsd":
|
||||
return os.Readlink("/proc/curproc/exe")
|
||||
case "dragonfly":
|
||||
return os.Readlink("/proc/curproc/file")
|
||||
case "solaris":
|
||||
return os.Readlink(fmt.Sprintf("/proc/%d/path/a.out", os.Getpid()))
|
||||
}
|
||||
return "", errors.New("ExecPath not implemented for " + runtime.GOOS)
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !go1.8,darwin !go1.8,freebsd openbsd
|
||||
|
||||
package osext
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var initCwd, initCwdErr = os.Getwd()
|
||||
|
||||
func executable() (string, error) {
|
||||
var mib [4]int32
|
||||
switch runtime.GOOS {
|
||||
case "freebsd":
|
||||
mib = [4]int32{1 /* CTL_KERN */, 14 /* KERN_PROC */, 12 /* KERN_PROC_PATHNAME */, -1}
|
||||
case "darwin":
|
||||
mib = [4]int32{1 /* CTL_KERN */, 38 /* KERN_PROCARGS */, int32(os.Getpid()), -1}
|
||||
case "openbsd":
|
||||
mib = [4]int32{1 /* CTL_KERN */, 55 /* KERN_PROC_ARGS */, int32(os.Getpid()), 1 /* KERN_PROC_ARGV */}
|
||||
}
|
||||
|
||||
n := uintptr(0)
|
||||
// Get length.
|
||||
_, _, errNum := syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), 4, 0, uintptr(unsafe.Pointer(&n)), 0, 0)
|
||||
if errNum != 0 {
|
||||
return "", errNum
|
||||
}
|
||||
if n == 0 { // This shouldn't happen.
|
||||
return "", nil
|
||||
}
|
||||
buf := make([]byte, n)
|
||||
_, _, errNum = syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), 4, uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&n)), 0, 0)
|
||||
if errNum != 0 {
|
||||
return "", errNum
|
||||
}
|
||||
if n == 0 { // This shouldn't happen.
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var execPath string
|
||||
switch runtime.GOOS {
|
||||
case "openbsd":
|
||||
// buf now contains **argv, with pointers to each of the C-style
|
||||
// NULL terminated arguments.
|
||||
var args []string
|
||||
argv := uintptr(unsafe.Pointer(&buf[0]))
|
||||
Loop:
|
||||
for {
|
||||
argp := *(**[1 << 20]byte)(unsafe.Pointer(argv))
|
||||
if argp == nil {
|
||||
break
|
||||
}
|
||||
for i := 0; uintptr(i) < n; i++ {
|
||||
// we don't want the full arguments list
|
||||
if string(argp[i]) == " " {
|
||||
break Loop
|
||||
}
|
||||
if argp[i] != 0 {
|
||||
continue
|
||||
}
|
||||
args = append(args, string(argp[:i]))
|
||||
n -= uintptr(i)
|
||||
break
|
||||
}
|
||||
if n < unsafe.Sizeof(argv) {
|
||||
break
|
||||
}
|
||||
argv += unsafe.Sizeof(argv)
|
||||
n -= unsafe.Sizeof(argv)
|
||||
}
|
||||
execPath = args[0]
|
||||
// There is no canonical way to get an executable path on
|
||||
// OpenBSD, so check PATH in case we are called directly
|
||||
if execPath[0] != '/' && execPath[0] != '.' {
|
||||
execIsInPath, err := exec.LookPath(execPath)
|
||||
if err == nil {
|
||||
execPath = execIsInPath
|
||||
}
|
||||
}
|
||||
default:
|
||||
for i, v := range buf {
|
||||
if v == 0 {
|
||||
buf = buf[:i]
|
||||
break
|
||||
}
|
||||
}
|
||||
execPath = string(buf)
|
||||
}
|
||||
|
||||
var err error
|
||||
// execPath will not be empty due to above checks.
|
||||
// Try to get the absolute path if the execPath is not rooted.
|
||||
if execPath[0] != '/' {
|
||||
execPath, err = getAbs(execPath)
|
||||
if err != nil {
|
||||
return execPath, err
|
||||
}
|
||||
}
|
||||
// For darwin KERN_PROCARGS may return the path to a symlink rather than the
|
||||
// actual executable.
|
||||
if runtime.GOOS == "darwin" {
|
||||
if execPath, err = filepath.EvalSymlinks(execPath); err != nil {
|
||||
return execPath, err
|
||||
}
|
||||
}
|
||||
return execPath, nil
|
||||
}
|
||||
|
||||
func getAbs(execPath string) (string, error) {
|
||||
if initCwdErr != nil {
|
||||
return execPath, initCwdErr
|
||||
}
|
||||
// The execPath may begin with a "../" or a "./" so clean it first.
|
||||
// Join the two paths, trailing and starting slashes undetermined, so use
|
||||
// the generic Join function.
|
||||
return filepath.Join(initCwd, filepath.Clean(execPath)), nil
|
||||
}
|
|
@ -0,0 +1,203 @@
|
|||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin linux freebsd netbsd windows openbsd
|
||||
|
||||
package osext
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
executableEnvVar = "OSTEST_OUTPUT_EXECUTABLE"
|
||||
|
||||
executableEnvValueMatch = "match"
|
||||
executableEnvValueDelete = "delete"
|
||||
)
|
||||
|
||||
func TestPrintExecutable(t *testing.T) {
|
||||
ef, err := Executable()
|
||||
if err != nil {
|
||||
t.Fatalf("Executable failed: %v", err)
|
||||
}
|
||||
t.Log("Executable:", ef)
|
||||
}
|
||||
func TestPrintExecutableFolder(t *testing.T) {
|
||||
ef, err := ExecutableFolder()
|
||||
if err != nil {
|
||||
t.Fatalf("ExecutableFolder failed: %v", err)
|
||||
}
|
||||
t.Log("Executable Folder:", ef)
|
||||
}
|
||||
func TestExecutableFolder(t *testing.T) {
|
||||
ef, err := ExecutableFolder()
|
||||
if err != nil {
|
||||
t.Fatalf("ExecutableFolder failed: %v", err)
|
||||
}
|
||||
if ef[len(ef)-1] == filepath.Separator {
|
||||
t.Fatal("ExecutableFolder ends with a trailing slash.")
|
||||
}
|
||||
}
|
||||
func TestExecutableMatch(t *testing.T) {
|
||||
ep, err := Executable()
|
||||
if err != nil {
|
||||
t.Fatalf("Executable failed: %v", err)
|
||||
}
|
||||
|
||||
// fullpath to be of the form "dir/prog".
|
||||
dir := filepath.Dir(filepath.Dir(ep))
|
||||
fullpath, err := filepath.Rel(dir, ep)
|
||||
if err != nil {
|
||||
t.Fatalf("filepath.Rel: %v", err)
|
||||
}
|
||||
// Make child start with a relative program path.
|
||||
// Alter argv[0] for child to verify getting real path without argv[0].
|
||||
cmd := &exec.Cmd{
|
||||
Dir: dir,
|
||||
Path: fullpath,
|
||||
Env: []string{fmt.Sprintf("%s=%s", executableEnvVar, executableEnvValueMatch)},
|
||||
}
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("exec(self) failed: %v", err)
|
||||
}
|
||||
outs := string(out)
|
||||
if !filepath.IsAbs(outs) {
|
||||
t.Fatalf("Child returned %q, want an absolute path", out)
|
||||
}
|
||||
if !sameFile(outs, ep) {
|
||||
t.Fatalf("Child returned %q, not the same file as %q", out, ep)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecutableDelete(t *testing.T) {
|
||||
if runtime.GOOS != "linux" {
|
||||
t.Skip()
|
||||
}
|
||||
fpath, err := Executable()
|
||||
if err != nil {
|
||||
t.Fatalf("Executable failed: %v", err)
|
||||
}
|
||||
|
||||
r, w := io.Pipe()
|
||||
stderrBuff := &bytes.Buffer{}
|
||||
stdoutBuff := &bytes.Buffer{}
|
||||
cmd := &exec.Cmd{
|
||||
Path: fpath,
|
||||
Env: []string{fmt.Sprintf("%s=%s", executableEnvVar, executableEnvValueDelete)},
|
||||
Stdin: r,
|
||||
Stderr: stderrBuff,
|
||||
Stdout: stdoutBuff,
|
||||
}
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("exec(self) start failed: %v", err)
|
||||
}
|
||||
|
||||
tempPath := fpath + "_copy"
|
||||
_ = os.Remove(tempPath)
|
||||
|
||||
err = copyFile(tempPath, fpath)
|
||||
if err != nil {
|
||||
t.Fatalf("copy file failed: %v", err)
|
||||
}
|
||||
err = os.Remove(fpath)
|
||||
if err != nil {
|
||||
t.Fatalf("remove running test file failed: %v", err)
|
||||
}
|
||||
err = os.Rename(tempPath, fpath)
|
||||
if err != nil {
|
||||
t.Fatalf("rename copy to previous name failed: %v", err)
|
||||
}
|
||||
|
||||
w.Write([]byte{0})
|
||||
w.Close()
|
||||
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
t.Fatalf("exec wait failed: %v", err)
|
||||
}
|
||||
|
||||
childPath := stderrBuff.String()
|
||||
if !filepath.IsAbs(childPath) {
|
||||
t.Fatalf("Child returned %q, want an absolute path", childPath)
|
||||
}
|
||||
if !sameFile(childPath, fpath) {
|
||||
t.Fatalf("Child returned %q, not the same file as %q", childPath, fpath)
|
||||
}
|
||||
}
|
||||
|
||||
func sameFile(fn1, fn2 string) bool {
|
||||
fi1, err := os.Stat(fn1)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
fi2, err := os.Stat(fn2)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return os.SameFile(fi1, fi2)
|
||||
}
|
||||
func copyFile(dest, src string) error {
|
||||
df, err := os.Create(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer df.Close()
|
||||
|
||||
sf, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sf.Close()
|
||||
|
||||
_, err = io.Copy(df, sf)
|
||||
return err
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
env := os.Getenv(executableEnvVar)
|
||||
switch env {
|
||||
case "":
|
||||
os.Exit(m.Run())
|
||||
case executableEnvValueMatch:
|
||||
// First chdir to another path.
|
||||
dir := "/"
|
||||
if runtime.GOOS == "windows" {
|
||||
dir = filepath.VolumeName(".")
|
||||
}
|
||||
os.Chdir(dir)
|
||||
if ep, err := Executable(); err != nil {
|
||||
fmt.Fprint(os.Stderr, "ERROR: ", err)
|
||||
} else {
|
||||
fmt.Fprint(os.Stderr, ep)
|
||||
}
|
||||
case executableEnvValueDelete:
|
||||
bb := make([]byte, 1)
|
||||
var err error
|
||||
n, err := os.Stdin.Read(bb)
|
||||
if err != nil {
|
||||
fmt.Fprint(os.Stderr, "ERROR: ", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
if n != 1 {
|
||||
fmt.Fprint(os.Stderr, "ERROR: n != 1, n == ", n)
|
||||
os.Exit(2)
|
||||
}
|
||||
if ep, err := Executable(); err != nil {
|
||||
fmt.Fprint(os.Stderr, "ERROR: ", err)
|
||||
} else {
|
||||
fmt.Fprint(os.Stderr, ep)
|
||||
}
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//+build !go1.8
|
||||
|
||||
package osext
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unicode/utf16"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
kernel = syscall.MustLoadDLL("kernel32.dll")
|
||||
getModuleFileNameProc = kernel.MustFindProc("GetModuleFileNameW")
|
||||
)
|
||||
|
||||
// GetModuleFileName() with hModule = NULL
|
||||
func executable() (exePath string, err error) {
|
||||
return getModuleFileName()
|
||||
}
|
||||
|
||||
func getModuleFileName() (string, error) {
|
||||
var n uint32
|
||||
b := make([]uint16, syscall.MAX_PATH)
|
||||
size := uint32(len(b))
|
||||
|
||||
r0, _, e1 := getModuleFileNameProc.Call(0, uintptr(unsafe.Pointer(&b[0])), uintptr(size))
|
||||
n = uint32(r0)
|
||||
if n == 0 {
|
||||
return "", e1
|
||||
}
|
||||
return string(utf16.Decode(b[0:n])), nil
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
language: go
|
||||
go_import_path: github.com/kardianos/service
|
||||
sudo: required
|
||||
|
||||
go:
|
||||
- 1.6
|
||||
- tip
|
||||
|
||||
before_install:
|
||||
- go get github.com/mattn/goveralls
|
||||
- go get golang.org/x/tools/cmd/cover
|
||||
|
||||
script:
|
||||
- chmod +x linux-test-su.sh
|
||||
- sudo ./linux-test-su.sh $GOPATH `which go`
|
||||
- $GOPATH/bin/goveralls -service=travis-ci
|
|
@ -0,0 +1,20 @@
|
|||
Copyright (c) 2015 Daniel Theophanes
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
|
||||
3. This notice may not be removed or altered from any source
|
||||
distribution.
|
|
@ -0,0 +1,14 @@
|
|||
# service [![GoDoc](https://godoc.org/github.com/kardianos/service?status.svg)](https://godoc.org/github.com/kardianos/service)
|
||||
|
||||
service will install / un-install, start / stop, and run a program as a service (daemon).
|
||||
Currently supports Windows XP+, Linux/(systemd | Upstart | SysV), and OSX/Launchd.
|
||||
|
||||
Windows controls services by setting up callbacks that is non-trivial. This
|
||||
is very different then other systems. This package provides the same API
|
||||
despite the substantial differences.
|
||||
It also can be used to detect how a program is called, from an interactive
|
||||
terminal or from a service manager.
|
||||
|
||||
## BUGS
|
||||
* Dependencies field is not implemented for Linux systems and Launchd.
|
||||
* OS X when running as a UserService Interactive will not be accurate.
|
|
@ -0,0 +1,21 @@
|
|||
version: "{build}"
|
||||
|
||||
platform:
|
||||
- x86
|
||||
- x64
|
||||
|
||||
clone_folder: c:\gopath\src\github.com\kardianos\service
|
||||
|
||||
environment:
|
||||
GOPATH: c:\gopath
|
||||
|
||||
install:
|
||||
- go version
|
||||
- go env
|
||||
- go get -v -t ./...
|
||||
|
||||
build_script:
|
||||
- go install -v ./...
|
||||
|
||||
test_script:
|
||||
- go test -v -tags su ./...
|
|
@ -0,0 +1,48 @@
|
|||
// Copyright 2015 Daniel Theophanes.
|
||||
// Use of this source code is governed by a zlib-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
// ConsoleLogger logs to the std err.
|
||||
var ConsoleLogger = consoleLogger{}
|
||||
|
||||
type consoleLogger struct {
|
||||
info, warn, err *log.Logger
|
||||
}
|
||||
|
||||
func init() {
|
||||
ConsoleLogger.info = log.New(os.Stderr, "I: ", log.Ltime)
|
||||
ConsoleLogger.warn = log.New(os.Stderr, "W: ", log.Ltime)
|
||||
ConsoleLogger.err = log.New(os.Stderr, "E: ", log.Ltime)
|
||||
}
|
||||
|
||||
func (c consoleLogger) Error(v ...interface{}) error {
|
||||
c.err.Print(v...)
|
||||
return nil
|
||||
}
|
||||
func (c consoleLogger) Warning(v ...interface{}) error {
|
||||
c.warn.Print(v...)
|
||||
return nil
|
||||
}
|
||||
func (c consoleLogger) Info(v ...interface{}) error {
|
||||
c.info.Print(v...)
|
||||
return nil
|
||||
}
|
||||
func (c consoleLogger) Errorf(format string, a ...interface{}) error {
|
||||
c.err.Printf(format, a...)
|
||||
return nil
|
||||
}
|
||||
func (c consoleLogger) Warningf(format string, a ...interface{}) error {
|
||||
c.warn.Printf(format, a...)
|
||||
return nil
|
||||
}
|
||||
func (c consoleLogger) Infof(format string, a ...interface{}) error {
|
||||
c.info.Printf(format, a...)
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
// Copyright 2015 Daniel Theophanes.
|
||||
// Use of this source code is governed by a zlib-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Simple service that only works by printing a log message every few seconds.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
)
|
||||
|
||||
var logger service.Logger
|
||||
|
||||
// Program structures.
|
||||
// Define Start and Stop methods.
|
||||
type program struct {
|
||||
exit chan struct{}
|
||||
}
|
||||
|
||||
func (p *program) Start(s service.Service) error {
|
||||
if service.Interactive() {
|
||||
logger.Info("Running in terminal.")
|
||||
} else {
|
||||
logger.Info("Running under service manager.")
|
||||
}
|
||||
p.exit = make(chan struct{})
|
||||
|
||||
// Start should not block. Do the actual work async.
|
||||
go p.run()
|
||||
return nil
|
||||
}
|
||||
func (p *program) run() error {
|
||||
logger.Infof("I'm running %v.", service.Platform())
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case tm := <-ticker.C:
|
||||
logger.Infof("Still running at %v...", tm)
|
||||
case <-p.exit:
|
||||
ticker.Stop()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
func (p *program) Stop(s service.Service) error {
|
||||
// Any work in Stop should be quick, usually a few seconds at most.
|
||||
logger.Info("I'm Stopping!")
|
||||
close(p.exit)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Service setup.
|
||||
// Define service config.
|
||||
// Create the service.
|
||||
// Setup the logger.
|
||||
// Handle service controls (optional).
|
||||
// Run the service.
|
||||
func main() {
|
||||
svcFlag := flag.String("service", "", "Control the system service.")
|
||||
flag.Parse()
|
||||
|
||||
svcConfig := &service.Config{
|
||||
Name: "GoServiceExampleLogging",
|
||||
DisplayName: "Go Service Example for Logging",
|
||||
Description: "This is an example Go service that outputs log messages.",
|
||||
}
|
||||
|
||||
prg := &program{}
|
||||
s, err := service.New(prg, svcConfig)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
errs := make(chan error, 5)
|
||||
logger, err = s.Logger(errs)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
err := <-errs
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if len(*svcFlag) != 0 {
|
||||
err := service.Control(s, *svcFlag)
|
||||
if err != nil {
|
||||
log.Printf("Valid actions: %q\n", service.ControlAction)
|
||||
log.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
err = s.Run()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
// Copyright 2015 Daniel Theophanes.
|
||||
// Use of this source code is governed by a zlib-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Simple service that only works by printing a log message every few seconds.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/kardianos/osext"
|
||||
"github.com/kardianos/service"
|
||||
)
|
||||
|
||||
// Config is the runner app config structure.
|
||||
type Config struct {
|
||||
Name, DisplayName, Description string
|
||||
|
||||
Dir string
|
||||
Exec string
|
||||
Args []string
|
||||
Env []string
|
||||
|
||||
Stderr, Stdout string
|
||||
}
|
||||
|
||||
var logger service.Logger
|
||||
|
||||
type program struct {
|
||||
exit chan struct{}
|
||||
service service.Service
|
||||
|
||||
*Config
|
||||
|
||||
cmd *exec.Cmd
|
||||
}
|
||||
|
||||
func (p *program) Start(s service.Service) error {
|
||||
// Look for exec.
|
||||
// Verify home directory.
|
||||
fullExec, err := exec.LookPath(p.Exec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to find executable %q: %v", p.Exec, err)
|
||||
}
|
||||
|
||||
p.cmd = exec.Command(fullExec, p.Args...)
|
||||
p.cmd.Dir = p.Dir
|
||||
p.cmd.Env = append(os.Environ(), p.Env...)
|
||||
|
||||
go p.run()
|
||||
return nil
|
||||
}
|
||||
func (p *program) run() {
|
||||
logger.Info("Starting ", p.DisplayName)
|
||||
defer func() {
|
||||
if service.Interactive() {
|
||||
p.Stop(p.service)
|
||||
} else {
|
||||
p.service.Stop()
|
||||
}
|
||||
}()
|
||||
|
||||
if p.Stderr != "" {
|
||||
f, err := os.OpenFile(p.Stderr, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0777)
|
||||
if err != nil {
|
||||
logger.Warningf("Failed to open std err %q: %v", p.Stderr, err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
p.cmd.Stderr = f
|
||||
}
|
||||
if p.Stdout != "" {
|
||||
f, err := os.OpenFile(p.Stdout, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0777)
|
||||
if err != nil {
|
||||
logger.Warningf("Failed to open std out %q: %v", p.Stdout, err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
p.cmd.Stdout = f
|
||||
}
|
||||
|
||||
err := p.cmd.Run()
|
||||
if err != nil {
|
||||
logger.Warningf("Error running: %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
func (p *program) Stop(s service.Service) error {
|
||||
close(p.exit)
|
||||
logger.Info("Stopping ", p.DisplayName)
|
||||
if p.cmd.ProcessState.Exited() == false {
|
||||
p.cmd.Process.Kill()
|
||||
}
|
||||
if service.Interactive() {
|
||||
os.Exit(0)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getConfigPath() (string, error) {
|
||||
fullexecpath, err := osext.Executable()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
dir, execname := filepath.Split(fullexecpath)
|
||||
ext := filepath.Ext(execname)
|
||||
name := execname[:len(execname)-len(ext)]
|
||||
|
||||
return filepath.Join(dir, name+".json"), nil
|
||||
}
|
||||
|
||||
func getConfig(path string) (*Config, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
conf := &Config{}
|
||||
|
||||
r := json.NewDecoder(f)
|
||||
err = r.Decode(&conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
svcFlag := flag.String("service", "", "Control the system service.")
|
||||
flag.Parse()
|
||||
|
||||
configPath, err := getConfigPath()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
config, err := getConfig(configPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
svcConfig := &service.Config{
|
||||
Name: config.Name,
|
||||
DisplayName: config.DisplayName,
|
||||
Description: config.Description,
|
||||
}
|
||||
|
||||
prg := &program{
|
||||
exit: make(chan struct{}),
|
||||
|
||||
Config: config,
|
||||
}
|
||||
s, err := service.New(prg, svcConfig)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
prg.service = s
|
||||
|
||||
errs := make(chan error, 5)
|
||||
logger, err = s.Logger(errs)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
err := <-errs
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if len(*svcFlag) != 0 {
|
||||
err := service.Control(s, *svcFlag)
|
||||
if err != nil {
|
||||
log.Printf("Valid actions: %q\n", service.ControlAction)
|
||||
log.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
err = s.Run()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"Name": "builder",
|
||||
"DisplayName": "Go Builder",
|
||||
"Description": "Run the Go Builder",
|
||||
|
||||
"Dir": "C:\\dev\\go\\src",
|
||||
"Exec": "C:\\windows\\system32\\cmd.exe",
|
||||
"Args": ["/C","C:\\dev\\go\\src\\all.bat"],
|
||||
"Env": [
|
||||
"PATH=C:\\TDM-GCC-64\\bin;C:\\Program Files (x86)\\Git\\cmd",
|
||||
"GOROOT_BOOTSTRAP=C:\\dev\\go_ready",
|
||||
"HOMEDRIVE=C:",
|
||||
"HOMEPATH=\\Documents and Settings\\Administrator"
|
||||
],
|
||||
|
||||
"Stderr": "C:\\builder_err.log",
|
||||
"Stdout": "C:\\builder_out.log"
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
// Copyright 2015 Daniel Theophanes.
|
||||
// Use of this source code is governed by a zlib-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// simple does nothing except block while running the service.
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
)
|
||||
|
||||
var logger service.Logger
|
||||
|
||||
type program struct{}
|
||||
|
||||
func (p *program) Start(s service.Service) error {
|
||||
// Start should not block. Do the actual work async.
|
||||
go p.run()
|
||||
return nil
|
||||
}
|
||||
func (p *program) run() {
|
||||
// Do work here
|
||||
}
|
||||
func (p *program) Stop(s service.Service) error {
|
||||
// Stop should not block. Return with a few seconds.
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
svcConfig := &service.Config{
|
||||
Name: "GoServiceExampleSimple",
|
||||
DisplayName: "Go Service Example",
|
||||
Description: "This is an example Go service.",
|
||||
}
|
||||
|
||||
prg := &program{}
|
||||
s, err := service.New(prg, svcConfig)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
logger, err = s.Logger(nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = s.Run()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
// Copyright 2015 Daniel Theophanes.
|
||||
// Use of this source code is governed by a zlib-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// simple does nothing except block while running the service.
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
)
|
||||
|
||||
var logger service.Logger
|
||||
|
||||
type program struct{}
|
||||
|
||||
func (p *program) Start(s service.Service) error {
|
||||
// Start should not block. Do the actual work async.
|
||||
go p.run()
|
||||
return nil
|
||||
}
|
||||
func (p *program) run() {
|
||||
// Do work here
|
||||
}
|
||||
func (p *program) Stop(s service.Service) error {
|
||||
// Stop should not block. Return with a few seconds.
|
||||
<-time.After(time.Second * 13)
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
svcConfig := &service.Config{
|
||||
Name: "GoServiceExampleStopPause",
|
||||
DisplayName: "Go Service Example: Stop Pause",
|
||||
Description: "This is an example Go service that pauses on stop.",
|
||||
}
|
||||
|
||||
prg := &program{}
|
||||
s, err := service.New(prg, svcConfig)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if len(os.Args) > 1 {
|
||||
err = service.Control(s, os.Args[1])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
logger, err = s.Logger(nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = s.Run()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
#!/usr/bin/env bash
|
||||
# This script is used to run the tests under linux as root
|
||||
#
|
||||
# Usage:
|
||||
# linux-test-su.sh goPath goBinPath
|
||||
#
|
||||
# goPath is the standard GOPATH
|
||||
# goBinPath is the location of go
|
||||
#
|
||||
# Typical usage:
|
||||
# sudo ./linux-test-su.sh $GOPATH `which go`
|
||||
|
||||
export GOPATH=$1
|
||||
export GOROOT=`dirname $(dirname $2)`
|
||||
$GOROOT/bin/go test -v -tags su ./...
|
|
@ -0,0 +1,35 @@
|
|||
|
||||
all: sysv systemd upstart clean
|
||||
|
||||
test:
|
||||
@go test -c ..
|
||||
|
||||
clean:
|
||||
-rm service.test
|
||||
|
||||
sysv: test
|
||||
@echo sysv
|
||||
@cp service.test sysv/
|
||||
@docker build -q --tag="service.test.sysv" sysv
|
||||
@-docker run service.test.sysv
|
||||
@-docker rm $(shell docker ps -l -q)
|
||||
@-docker rmi -f service.test.sysv
|
||||
@-rm sysv/service.test
|
||||
|
||||
systemd: test
|
||||
@echo systemd
|
||||
@cp service.test systemd/
|
||||
@docker build -q --tag="service.test.systemd" systemd
|
||||
@-docker run --privileged -v /sys/fs/cgroup:/sys/fs/cgroup:ro service.test.systemd
|
||||
@-docker rm $(shell docker ps -l -q)
|
||||
@-docker rmi -f service.test.systemd
|
||||
@-rm systemd/service.test
|
||||
|
||||
upstart: test
|
||||
@echo upstart
|
||||
@cp service.test upstart/
|
||||
@docker build -q --tag="service.test.upstart" upstart
|
||||
@-docker run service.test.upstart
|
||||
@-docker rm $(shell docker ps -l -q)
|
||||
@-docker rmi -f service.test.upstart
|
||||
@-rm upstart/service.test
|
|
@ -0,0 +1,6 @@
|
|||
## Docker images to help vet linux configurations.
|
||||
|
||||
Although the actual init systems won't run in docker,
|
||||
it may still be usefull to attempt to run it in different
|
||||
environments.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
FROM minimum2scp/systemd:latest
|
||||
ADD service.test /tmp/
|
||||
CMD /tmp/service.test -test.v=true
|
|
@ -0,0 +1,3 @@
|
|||
FROM minimum2scp/baseimage:latest
|
||||
ADD service.test /tmp/
|
||||
CMD /tmp/service.test -test.v=true
|
|
@ -0,0 +1,3 @@
|
|||
FROM ubuntu:14.04
|
||||
ADD service.test /tmp/
|
||||
CMD /tmp/service.test -test.v=true
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright 2015 Daniel Theophanes.
|
||||
// Use of this source code is governed by a zlib-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPlatformName(t *testing.T) {
|
||||
got := Platform()
|
||||
t.Logf("Platform is %v", got)
|
||||
wantPrefix := runtime.GOOS + "-"
|
||||
if !strings.HasPrefix(got, wantPrefix) {
|
||||
t.Errorf("Platform() want: /^%s.*$/, got: %s", wantPrefix, got)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
//+build !go1.8
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/kardianos/osext"
|
||||
)
|
||||
|
||||
func (c *Config) execPath() (string, error) {
|
||||
if len(c.Executable) != 0 {
|
||||
return filepath.Abs(c.Executable)
|
||||
}
|
||||
return osext.Executable()
|
||||
}
|
|
@ -0,0 +1,362 @@
|
|||
// Copyright 2015 Daniel Theophanes.
|
||||
// Use of this source code is governed by a zlib-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package service provides a simple way to create a system service.
|
||||
// Currently supports Windows, Linux/(systemd | Upstart | SysV), and OSX/Launchd.
|
||||
//
|
||||
// Windows controls services by setting up callbacks that is non-trivial. This
|
||||
// is very different then other systems. This package provides the same API
|
||||
// despite the substantial differences.
|
||||
// It also can be used to detect how a program is called, from an interactive
|
||||
// terminal or from a service manager.
|
||||
//
|
||||
// Examples in the example/ folder.
|
||||
//
|
||||
// package main
|
||||
//
|
||||
// import (
|
||||
// "log"
|
||||
//
|
||||
// "github.com/kardianos/service"
|
||||
// )
|
||||
//
|
||||
// var logger service.Logger
|
||||
//
|
||||
// type program struct{}
|
||||
//
|
||||
// func (p *program) Start(s service.Service) error {
|
||||
// // Start should not block. Do the actual work async.
|
||||
// go p.run()
|
||||
// return nil
|
||||
// }
|
||||
// func (p *program) run() {
|
||||
// // Do work here
|
||||
// }
|
||||
// func (p *program) Stop(s service.Service) error {
|
||||
// // Stop should not block. Return with a few seconds.
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// func main() {
|
||||
// svcConfig := &service.Config{
|
||||
// Name: "GoServiceTest",
|
||||
// DisplayName: "Go Service Test",
|
||||
// Description: "This is a test Go service.",
|
||||
// }
|
||||
//
|
||||
// prg := &program{}
|
||||
// s, err := service.New(prg, svcConfig)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// logger, err = s.Logger(nil)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// err = s.Run()
|
||||
// if err != nil {
|
||||
// logger.Error(err)
|
||||
// }
|
||||
// }
|
||||
package service // import "github.com/kardianos/service"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
optionKeepAlive = "KeepAlive"
|
||||
optionKeepAliveDefault = true
|
||||
optionRunAtLoad = "RunAtLoad"
|
||||
optionRunAtLoadDefault = false
|
||||
optionUserService = "UserService"
|
||||
optionUserServiceDefault = false
|
||||
optionSessionCreate = "SessionCreate"
|
||||
optionSessionCreateDefault = false
|
||||
|
||||
optionRunWait = "RunWait"
|
||||
optionReloadSignal = "ReloadSignal"
|
||||
optionPIDFile = "PIDFile"
|
||||
)
|
||||
|
||||
// Config provides the setup for a Service. The Name field is required.
|
||||
type Config struct {
|
||||
Name string // Required name of the service. No spaces suggested.
|
||||
DisplayName string // Display name, spaces allowed.
|
||||
Description string // Long description of service.
|
||||
UserName string // Run as username.
|
||||
Arguments []string // Run with arguments.
|
||||
|
||||
// Optional field to specify the executable for service.
|
||||
// If empty the current executable is used.
|
||||
Executable string
|
||||
|
||||
// Array of service dependencies.
|
||||
// Not yet implemented on Linux or OS X.
|
||||
Dependencies []string
|
||||
|
||||
// The following fields are not supported on Windows.
|
||||
WorkingDirectory string // Initial working directory.
|
||||
ChRoot string
|
||||
|
||||
// System specific options.
|
||||
// * OS X
|
||||
// - KeepAlive bool (true)
|
||||
// - RunAtLoad bool (false)
|
||||
// - UserService bool (false) - Install as a current user service.
|
||||
// - SessionCreate bool (false) - Create a full user session.
|
||||
// * POSIX
|
||||
// - RunWait func() (wait for SIGNAL) - Do not install signal but wait for this function to return.
|
||||
// - ReloadSignal string () [USR1, ...] - Signal to send on reaload.
|
||||
// - PIDFile string () [/run/prog.pid] - Location of the PID file.
|
||||
Option KeyValue
|
||||
}
|
||||
|
||||
var (
|
||||
system System
|
||||
systemRegistry []System
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNameFieldRequired is returned when Conifg.Name is empty.
|
||||
ErrNameFieldRequired = errors.New("Config.Name field is required.")
|
||||
// ErrNoServiceSystemDetected is returned when no system was detected.
|
||||
ErrNoServiceSystemDetected = errors.New("No service system detected.")
|
||||
)
|
||||
|
||||
// New creates a new service based on a service interface and configuration.
|
||||
func New(i Interface, c *Config) (Service, error) {
|
||||
if len(c.Name) == 0 {
|
||||
return nil, ErrNameFieldRequired
|
||||
}
|
||||
if system == nil {
|
||||
return nil, ErrNoServiceSystemDetected
|
||||
}
|
||||
return system.New(i, c)
|
||||
}
|
||||
|
||||
// KeyValue provides a list of platform specific options. See platform docs for
|
||||
// more details.
|
||||
type KeyValue map[string]interface{}
|
||||
|
||||
// bool returns the value of the given name, assuming the value is a boolean.
|
||||
// If the value isn't found or is not of the type, the defaultValue is returned.
|
||||
func (kv KeyValue) bool(name string, defaultValue bool) bool {
|
||||
if v, found := kv[name]; found {
|
||||
if castValue, is := v.(bool); is {
|
||||
return castValue
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// int returns the value of the given name, assuming the value is an int.
|
||||
// If the value isn't found or is not of the type, the defaultValue is returned.
|
||||
func (kv KeyValue) int(name string, defaultValue int) int {
|
||||
if v, found := kv[name]; found {
|
||||
if castValue, is := v.(int); is {
|
||||
return castValue
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// string returns the value of the given name, assuming the value is a string.
|
||||
// If the value isn't found or is not of the type, the defaultValue is returned.
|
||||
func (kv KeyValue) string(name string, defaultValue string) string {
|
||||
if v, found := kv[name]; found {
|
||||
if castValue, is := v.(string); is {
|
||||
return castValue
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// float64 returns the value of the given name, assuming the value is a float64.
|
||||
// If the value isn't found or is not of the type, the defaultValue is returned.
|
||||
func (kv KeyValue) float64(name string, defaultValue float64) float64 {
|
||||
if v, found := kv[name]; found {
|
||||
if castValue, is := v.(float64); is {
|
||||
return castValue
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// funcSingle returns the value of the given name, assuming the value is a float64.
|
||||
// If the value isn't found or is not of the type, the defaultValue is returned.
|
||||
func (kv KeyValue) funcSingle(name string, defaultValue func()) func() {
|
||||
if v, found := kv[name]; found {
|
||||
if castValue, is := v.(func()); is {
|
||||
return castValue
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// Platform returns a description of the system service.
|
||||
func Platform() string {
|
||||
if system == nil {
|
||||
return ""
|
||||
}
|
||||
return system.String()
|
||||
}
|
||||
|
||||
// Interactive returns false if running under the OS service manager
|
||||
// and true otherwise.
|
||||
func Interactive() bool {
|
||||
if system == nil {
|
||||
return true
|
||||
}
|
||||
return system.Interactive()
|
||||
}
|
||||
|
||||
func newSystem() System {
|
||||
for _, choice := range systemRegistry {
|
||||
if choice.Detect() == false {
|
||||
continue
|
||||
}
|
||||
return choice
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChooseSystem chooses a system from the given system services.
|
||||
// SystemServices are considered in the order they are suggested.
|
||||
// Calling this may change what Interactive and Platform return.
|
||||
func ChooseSystem(a ...System) {
|
||||
systemRegistry = a
|
||||
system = newSystem()
|
||||
}
|
||||
|
||||
// ChosenSystem returns the system that service will use.
|
||||
func ChosenSystem() System {
|
||||
return system
|
||||
}
|
||||
|
||||
// AvailableSystems returns the list of system services considered
|
||||
// when choosing the system service.
|
||||
func AvailableSystems() []System {
|
||||
return systemRegistry
|
||||
}
|
||||
|
||||
// System represents the service manager that is available.
|
||||
type System interface {
|
||||
// String returns a description of the system.
|
||||
String() string
|
||||
|
||||
// Detect returns true if the system is available to use.
|
||||
Detect() bool
|
||||
|
||||
// Interactive returns false if running under the system service manager
|
||||
// and true otherwise.
|
||||
Interactive() bool
|
||||
|
||||
// New creates a new service for this system.
|
||||
New(i Interface, c *Config) (Service, error)
|
||||
}
|
||||
|
||||
// Interface represents the service interface for a program. Start runs before
|
||||
// the hosting process is granted control and Stop runs when control is returned.
|
||||
//
|
||||
// 1. OS service manager executes user program.
|
||||
// 2. User program sees it is executed from a service manager (IsInteractive is false).
|
||||
// 3. User program calls Service.Run() which blocks.
|
||||
// 4. Interface.Start() is called and quickly returns.
|
||||
// 5. User program runs.
|
||||
// 6. OS service manager signals the user program to stop.
|
||||
// 7. Interface.Stop() is called and quickly returns.
|
||||
// - For a successful exit, os.Exit should not be called in Interface.Stop().
|
||||
// 8. Service.Run returns.
|
||||
// 9. User program should quickly exit.
|
||||
type Interface interface {
|
||||
// Start provides a place to initiate the service. The service doesn't not
|
||||
// signal a completed start until after this function returns, so the
|
||||
// Start function must not take more then a few seconds at most.
|
||||
Start(s Service) error
|
||||
|
||||
// Stop provides a place to clean up program execution before it is terminated.
|
||||
// It should not take more then a few seconds to execute.
|
||||
// Stop should not call os.Exit directly in the function.
|
||||
Stop(s Service) error
|
||||
}
|
||||
|
||||
// TODO: Add Configure to Service interface.
|
||||
|
||||
// Service represents a service that can be run or controlled.
|
||||
type Service interface {
|
||||
// Run should be called shortly after the program entry point.
|
||||
// After Interface.Stop has finished running, Run will stop blocking.
|
||||
// After Run stops blocking, the program must exit shortly after.
|
||||
Run() error
|
||||
|
||||
// Start signals to the OS service manager the given service should start.
|
||||
Start() error
|
||||
|
||||
// Stop signals to the OS service manager the given service should stop.
|
||||
Stop() error
|
||||
|
||||
// Restart signals to the OS service manager the given service should stop then start.
|
||||
Restart() error
|
||||
|
||||
// Install setups up the given service in the OS service manager. This may require
|
||||
// greater rights. Will return an error if it is already installed.
|
||||
Install() error
|
||||
|
||||
// Uninstall removes the given service from the OS service manager. This may require
|
||||
// greater rights. Will return an error if the service is not present.
|
||||
Uninstall() error
|
||||
|
||||
// Opens and returns a system logger. If the user program is running
|
||||
// interactively rather then as a service, the returned logger will write to
|
||||
// os.Stderr. If errs is non-nil errors will be sent on errs as well as
|
||||
// returned from Logger's functions.
|
||||
Logger(errs chan<- error) (Logger, error)
|
||||
|
||||
// SystemLogger opens and returns a system logger. If errs is non-nil errors
|
||||
// will be sent on errs as well as returned from Logger's functions.
|
||||
SystemLogger(errs chan<- error) (Logger, error)
|
||||
|
||||
// String displays the name of the service. The display name if present,
|
||||
// otherwise the name.
|
||||
String() string
|
||||
}
|
||||
|
||||
// ControlAction list valid string texts to use in Control.
|
||||
var ControlAction = [5]string{"start", "stop", "restart", "install", "uninstall"}
|
||||
|
||||
// Control issues control functions to the service from a given action string.
|
||||
func Control(s Service, action string) error {
|
||||
var err error
|
||||
switch action {
|
||||
case ControlAction[0]:
|
||||
err = s.Start()
|
||||
case ControlAction[1]:
|
||||
err = s.Stop()
|
||||
case ControlAction[2]:
|
||||
err = s.Restart()
|
||||
case ControlAction[3]:
|
||||
err = s.Install()
|
||||
case ControlAction[4]:
|
||||
err = s.Uninstall()
|
||||
default:
|
||||
err = fmt.Errorf("Unknown action %s", action)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to %s %v: %v", action, s, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Logger writes to the system log.
|
||||
type Logger interface {
|
||||
Error(v ...interface{}) error
|
||||
Warning(v ...interface{}) error
|
||||
Info(v ...interface{}) error
|
||||
|
||||
Errorf(format string, a ...interface{}) error
|
||||
Warningf(format string, a ...interface{}) error
|
||||
Infof(format string, a ...interface{}) error
|
||||
}
|
|
@ -0,0 +1,240 @@
|
|||
// Copyright 2015 Daniel Theophanes.
|
||||
// Use of this source code is governed by a zlib-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
const maxPathSize = 32 * 1024
|
||||
|
||||
const version = "darwin-launchd"
|
||||
|
||||
type darwinSystem struct{}
|
||||
|
||||
func (darwinSystem) String() string {
|
||||
return version
|
||||
}
|
||||
func (darwinSystem) Detect() bool {
|
||||
return true
|
||||
}
|
||||
func (darwinSystem) Interactive() bool {
|
||||
return interactive
|
||||
}
|
||||
func (darwinSystem) New(i Interface, c *Config) (Service, error) {
|
||||
s := &darwinLaunchdService{
|
||||
i: i,
|
||||
Config: c,
|
||||
|
||||
userService: c.Option.bool(optionUserService, optionUserServiceDefault),
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
ChooseSystem(darwinSystem{})
|
||||
}
|
||||
|
||||
var interactive = false
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
interactive, err = isInteractive()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func isInteractive() (bool, error) {
|
||||
// TODO: The PPID of Launchd is 1. The PPid of a service process should match launchd's PID.
|
||||
return os.Getppid() != 1, nil
|
||||
}
|
||||
|
||||
type darwinLaunchdService struct {
|
||||
i Interface
|
||||
*Config
|
||||
|
||||
userService bool
|
||||
}
|
||||
|
||||
func (s *darwinLaunchdService) String() string {
|
||||
if len(s.DisplayName) > 0 {
|
||||
return s.DisplayName
|
||||
}
|
||||
return s.Name
|
||||
}
|
||||
|
||||
func (s *darwinLaunchdService) getHomeDir() (string, error) {
|
||||
u, err := user.Current()
|
||||
if err == nil {
|
||||
return u.HomeDir, nil
|
||||
}
|
||||
|
||||
// alternate methods
|
||||
homeDir := os.Getenv("HOME") // *nix
|
||||
if homeDir == "" {
|
||||
return "", errors.New("User home directory not found.")
|
||||
}
|
||||
return homeDir, nil
|
||||
}
|
||||
|
||||
func (s *darwinLaunchdService) getServiceFilePath() (string, error) {
|
||||
if s.userService {
|
||||
homeDir, err := s.getHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return homeDir + "/Library/LaunchAgents/" + s.Name + ".plist", nil
|
||||
}
|
||||
return "/Library/LaunchDaemons/" + s.Name + ".plist", nil
|
||||
}
|
||||
|
||||
func (s *darwinLaunchdService) Install() error {
|
||||
confPath, err := s.getServiceFilePath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = os.Stat(confPath)
|
||||
if err == nil {
|
||||
return fmt.Errorf("Init already exists: %s", confPath)
|
||||
}
|
||||
|
||||
if s.userService {
|
||||
// Ensure that ~/Library/LaunchAgents exists.
|
||||
err = os.MkdirAll(filepath.Dir(confPath), 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
f, err := os.Create(confPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
path, err := s.execPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var to = &struct {
|
||||
*Config
|
||||
Path string
|
||||
|
||||
KeepAlive, RunAtLoad bool
|
||||
SessionCreate bool
|
||||
}{
|
||||
Config: s.Config,
|
||||
Path: path,
|
||||
KeepAlive: s.Option.bool(optionKeepAlive, optionKeepAliveDefault),
|
||||
RunAtLoad: s.Option.bool(optionRunAtLoad, optionRunAtLoadDefault),
|
||||
SessionCreate: s.Option.bool(optionSessionCreate, optionSessionCreateDefault),
|
||||
}
|
||||
|
||||
functions := template.FuncMap{
|
||||
"bool": func(v bool) string {
|
||||
if v {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
},
|
||||
}
|
||||
t := template.Must(template.New("launchdConfig").Funcs(functions).Parse(launchdConfig))
|
||||
return t.Execute(f, to)
|
||||
}
|
||||
|
||||
func (s *darwinLaunchdService) Uninstall() error {
|
||||
s.Stop()
|
||||
|
||||
confPath, err := s.getServiceFilePath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Remove(confPath)
|
||||
}
|
||||
|
||||
func (s *darwinLaunchdService) Start() error {
|
||||
confPath, err := s.getServiceFilePath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return run("launchctl", "load", confPath)
|
||||
}
|
||||
func (s *darwinLaunchdService) Stop() error {
|
||||
confPath, err := s.getServiceFilePath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return run("launchctl", "unload", confPath)
|
||||
}
|
||||
func (s *darwinLaunchdService) Restart() error {
|
||||
err := s.Stop()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
return s.Start()
|
||||
}
|
||||
|
||||
func (s *darwinLaunchdService) Run() error {
|
||||
var err error
|
||||
|
||||
err = s.i.Start(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.Option.funcSingle(optionRunWait, func() {
|
||||
var sigChan = make(chan os.Signal, 3)
|
||||
signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt)
|
||||
<-sigChan
|
||||
})()
|
||||
|
||||
return s.i.Stop(s)
|
||||
}
|
||||
|
||||
func (s *darwinLaunchdService) Logger(errs chan<- error) (Logger, error) {
|
||||
if interactive {
|
||||
return ConsoleLogger, nil
|
||||
}
|
||||
return s.SystemLogger(errs)
|
||||
}
|
||||
func (s *darwinLaunchdService) SystemLogger(errs chan<- error) (Logger, error) {
|
||||
return newSysLogger(s.Name, errs)
|
||||
}
|
||||
|
||||
var launchdConfig = `<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd" >
|
||||
<plist version='1.0'>
|
||||
<dict>
|
||||
<key>Label</key><string>{{html .Name}}</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{{html .Path}}</string>
|
||||
{{range .Config.Arguments}}
|
||||
<string>{{html .}}</string>
|
||||
{{end}}
|
||||
</array>
|
||||
{{if .UserName}}<key>UserName</key><string>{{html .UserName}}</string>{{end}}
|
||||
{{if .ChRoot}}<key>RootDirectory</key><string>{{html .ChRoot}}</string>{{end}}
|
||||
{{if .WorkingDirectory}}<key>WorkingDirectory</key><string>{{html .WorkingDirectory}}</string>{{end}}
|
||||
<key>SessionCreate</key><{{bool .SessionCreate}}/>
|
||||
<key>KeepAlive</key><{{bool .KeepAlive}}/>
|
||||
<key>RunAtLoad</key><{{bool .RunAtLoad}}/>
|
||||
<key>Disabled</key><false/>
|
||||
</dict>
|
||||
</plist>
|
||||
`
|
|
@ -0,0 +1,15 @@
|
|||
//+build go1.8
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func (c *Config) execPath() (string, error) {
|
||||
if len(c.Executable) != 0 {
|
||||
return filepath.Abs(c.Executable)
|
||||
}
|
||||
return os.Executable()
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
// Copyright 2015 Daniel Theophanes.
|
||||
// Use of this source code is governed by a zlib-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type linuxSystemService struct {
|
||||
name string
|
||||
detect func() bool
|
||||
interactive func() bool
|
||||
new func(i Interface, c *Config) (Service, error)
|
||||
}
|
||||
|
||||
func (sc linuxSystemService) String() string {
|
||||
return sc.name
|
||||
}
|
||||
func (sc linuxSystemService) Detect() bool {
|
||||
return sc.detect()
|
||||
}
|
||||
func (sc linuxSystemService) Interactive() bool {
|
||||
return sc.interactive()
|
||||
}
|
||||
func (sc linuxSystemService) New(i Interface, c *Config) (Service, error) {
|
||||
return sc.new(i, c)
|
||||
}
|
||||
|
||||
func init() {
|
||||
ChooseSystem(linuxSystemService{
|
||||
name: "linux-systemd",
|
||||
detect: isSystemd,
|
||||
interactive: func() bool {
|
||||
is, _ := isInteractive()
|
||||
return is
|
||||
},
|
||||
new: newSystemdService,
|
||||
},
|
||||
linuxSystemService{
|
||||
name: "linux-upstart",
|
||||
detect: isUpstart,
|
||||
interactive: func() bool {
|
||||
is, _ := isInteractive()
|
||||
return is
|
||||
},
|
||||
new: newUpstartService,
|
||||
},
|
||||
linuxSystemService{
|
||||
name: "unix-systemv",
|
||||
detect: func() bool { return true },
|
||||
interactive: func() bool {
|
||||
is, _ := isInteractive()
|
||||
return is
|
||||
},
|
||||
new: newSystemVService,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func isInteractive() (bool, error) {
|
||||
// TODO: This is not true for user services.
|
||||
return os.Getppid() != 1, nil
|
||||
}
|
||||
|
||||
var tf = map[string]interface{}{
|
||||
"cmd": func(s string) string {
|
||||
return `"` + strings.Replace(s, `"`, `\"`, -1) + `"`
|
||||
},
|
||||
"cmdEscape": func(s string) string {
|
||||
return strings.Replace(s, " ", `\x20`, -1)
|
||||
},
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
// Copyright 2016 Lawrence Woodman <lwoodman@vlifesystems.com>
|
||||
// Use of this source code is governed by a zlib-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !su
|
||||
|
||||
package service_test
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestInstallRunRestartStopRemove(t *testing.T) {
|
||||
t.Skip("skipping test as not running as root/admin (Build tag: su)")
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
// Copyright 2015 Daniel Theophanes.
|
||||
// Use of this source code is governed by a zlib-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This needs to be run as root/admin hence the reason there is a build tag
|
||||
// +build su
|
||||
|
||||
package service_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
)
|
||||
|
||||
const runAsServiceArg = "RunThisAsService"
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if len(os.Args) == 2 {
|
||||
os.Exit(m.Run())
|
||||
} else if len(os.Args) == 4 && os.Args[2] == runAsServiceArg {
|
||||
reportDir := os.Args[3]
|
||||
writeReport(reportDir, "call")
|
||||
runService()
|
||||
writeReport(reportDir, "finished")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
log.Fatalf("Invalid arguments: %v", os.Args)
|
||||
}
|
||||
|
||||
func TestInstallRunRestartStopRemove(t *testing.T) {
|
||||
p := &program{}
|
||||
reportDir := mustTempDir(t)
|
||||
defer os.RemoveAll(reportDir)
|
||||
|
||||
s := mustNewRunAsService(t, p, reportDir)
|
||||
_ = s.Uninstall()
|
||||
|
||||
if err := s.Install(); err != nil {
|
||||
t.Fatal("Install", err)
|
||||
}
|
||||
defer s.Uninstall()
|
||||
|
||||
if err := s.Start(); err != nil {
|
||||
t.Fatal("Start", err)
|
||||
}
|
||||
defer s.Stop()
|
||||
checkReport(t, reportDir, "Start()", 1, 0)
|
||||
|
||||
if err := s.Restart(); err != nil {
|
||||
t.Fatal("restart", err)
|
||||
}
|
||||
|
||||
checkReport(t, reportDir, "Restart()", 2, 1)
|
||||
p.numStopped = 0
|
||||
if err := s.Stop(); err != nil {
|
||||
t.Fatal("stop", err)
|
||||
}
|
||||
checkReport(t, reportDir, "Stop()", 2, 2)
|
||||
|
||||
if err := s.Uninstall(); err != nil {
|
||||
t.Fatal("uninstall", err)
|
||||
}
|
||||
}
|
||||
|
||||
func runService() {
|
||||
p := &program{}
|
||||
sc := &service.Config{
|
||||
Name: "go_service_test",
|
||||
}
|
||||
s, err := service.New(p, sc)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err = s.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustTempDir(t *testing.T) string {
|
||||
dir, err := ioutil.TempDir("", "servicetest")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
func writeReport(reportDir string, action string) {
|
||||
b := []byte("go_test_service_report")
|
||||
timeStamp := time.Now().UnixNano()
|
||||
err := ioutil.WriteFile(
|
||||
filepath.Join(reportDir, fmt.Sprintf("%d-%s", timeStamp, action)),
|
||||
b,
|
||||
0644,
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
var matchActionRegexp = regexp.MustCompile("^(\\d+-)([a-z]*)$")
|
||||
|
||||
func getReport(
|
||||
t *testing.T,
|
||||
reportDir string,
|
||||
) (numCalls int, numFinished int) {
|
||||
numCalls = 0
|
||||
numFinished = 0
|
||||
files, err := ioutil.ReadDir(reportDir)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadDir(%s) err: %s", reportDir, err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if matchActionRegexp.MatchString(file.Name()) {
|
||||
action := matchActionRegexp.ReplaceAllString(file.Name(), "$2")
|
||||
switch action {
|
||||
case "call":
|
||||
numCalls++
|
||||
case "finished":
|
||||
numFinished++
|
||||
default:
|
||||
t.Fatalf("getReport() found report with incorrect action: %s", action)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func checkReport(
|
||||
t *testing.T,
|
||||
reportDir string,
|
||||
msgPrefix string,
|
||||
wantNumCalled int,
|
||||
wantNumFinished int,
|
||||
) {
|
||||
var numCalled int
|
||||
var numFinished int
|
||||
for i := 0; i < 25; i++ {
|
||||
numCalled, numFinished = getReport(t, reportDir)
|
||||
<-time.After(200 * time.Millisecond)
|
||||
if numCalled == wantNumCalled && numFinished == wantNumFinished {
|
||||
return
|
||||
}
|
||||
}
|
||||
if numCalled != wantNumCalled {
|
||||
t.Fatalf("%s - numCalled: %d, want %d",
|
||||
msgPrefix, numCalled, wantNumCalled)
|
||||
}
|
||||
if numFinished != wantNumFinished {
|
||||
t.Fatalf("%s - numFinished: %d, want %d",
|
||||
msgPrefix, numFinished, wantNumFinished)
|
||||
}
|
||||
}
|
||||
|
||||
func mustNewRunAsService(
|
||||
t *testing.T,
|
||||
p *program,
|
||||
reportDir string,
|
||||
) service.Service {
|
||||
sc := &service.Config{
|
||||
Name: "go_service_test",
|
||||
Arguments: []string{"-test.v=true", runAsServiceArg, reportDir},
|
||||
}
|
||||
s, err := service.New(p, sc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return s
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
// Copyright 2015 Daniel Theophanes.
|
||||
// Use of this source code is governed by a zlib-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
func isSystemd() bool {
|
||||
if _, err := os.Stat("/run/systemd/system"); err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type systemd struct {
|
||||
i Interface
|
||||
*Config
|
||||
}
|
||||
|
||||
func newSystemdService(i Interface, c *Config) (Service, error) {
|
||||
s := &systemd{
|
||||
i: i,
|
||||
Config: c,
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *systemd) String() string {
|
||||
if len(s.DisplayName) > 0 {
|
||||
return s.DisplayName
|
||||
}
|
||||
return s.Name
|
||||
}
|
||||
|
||||
// Systemd services should be supported, but are not currently.
|
||||
var errNoUserServiceSystemd = errors.New("User services are not supported on systemd.")
|
||||
|
||||
func (s *systemd) configPath() (cp string, err error) {
|
||||
if s.Option.bool(optionUserService, optionUserServiceDefault) {
|
||||
err = errNoUserServiceSystemd
|
||||
return
|
||||
}
|
||||
cp = "/etc/systemd/system/" + s.Config.Name + ".service"
|
||||
return
|
||||
}
|
||||
func (s *systemd) template() *template.Template {
|
||||
return template.Must(template.New("").Funcs(tf).Parse(systemdScript))
|
||||
}
|
||||
|
||||
func (s *systemd) Install() error {
|
||||
confPath, err := s.configPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = os.Stat(confPath)
|
||||
if err == nil {
|
||||
return fmt.Errorf("Init already exists: %s", confPath)
|
||||
}
|
||||
|
||||
f, err := os.Create(confPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
path, err := s.execPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var to = &struct {
|
||||
*Config
|
||||
Path string
|
||||
ReloadSignal string
|
||||
PIDFile string
|
||||
}{
|
||||
s.Config,
|
||||
path,
|
||||
s.Option.string(optionReloadSignal, ""),
|
||||
s.Option.string(optionPIDFile, ""),
|
||||
}
|
||||
|
||||
err = s.template().Execute(f, to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = run("systemctl", "enable", s.Name+".service")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return run("systemctl", "daemon-reload")
|
||||
}
|
||||
|
||||
func (s *systemd) Uninstall() error {
|
||||
err := run("systemctl", "disable", s.Name+".service")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cp, err := s.configPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Remove(cp); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *systemd) Logger(errs chan<- error) (Logger, error) {
|
||||
if system.Interactive() {
|
||||
return ConsoleLogger, nil
|
||||
}
|
||||
return s.SystemLogger(errs)
|
||||
}
|
||||
func (s *systemd) SystemLogger(errs chan<- error) (Logger, error) {
|
||||
return newSysLogger(s.Name, errs)
|
||||
}
|
||||
|
||||
func (s *systemd) Run() (err error) {
|
||||
err = s.i.Start(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.Option.funcSingle(optionRunWait, func() {
|
||||
var sigChan = make(chan os.Signal, 3)
|
||||
signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt)
|
||||
<-sigChan
|
||||
})()
|
||||
|
||||
return s.i.Stop(s)
|
||||
}
|
||||
|
||||
func (s *systemd) Start() error {
|
||||
return run("systemctl", "start", s.Name+".service")
|
||||
}
|
||||
|
||||
func (s *systemd) Stop() error {
|
||||
return run("systemctl", "stop", s.Name+".service")
|
||||
}
|
||||
|
||||
func (s *systemd) Restart() error {
|
||||
return run("systemctl", "restart", s.Name+".service")
|
||||
}
|
||||
|
||||
const systemdScript = `[Unit]
|
||||
Description={{.Description}}
|
||||
ConditionFileIsExecutable={{.Path|cmdEscape}}
|
||||
|
||||
[Service]
|
||||
StartLimitInterval=5
|
||||
StartLimitBurst=10
|
||||
ExecStart={{.Path|cmdEscape}}{{range .Arguments}} {{.|cmd}}{{end}}
|
||||
{{if .ChRoot}}RootDirectory={{.ChRoot|cmd}}{{end}}
|
||||
{{if .WorkingDirectory}}WorkingDirectory={{.WorkingDirectory|cmdEscape}}{{end}}
|
||||
{{if .UserName}}User={{.UserName}}{{end}}
|
||||
{{if .ReloadSignal}}ExecReload=/bin/kill -{{.ReloadSignal}} "$MAINPID"{{end}}
|
||||
{{if .PIDFile}}PIDFile={{.PIDFile|cmd}}{{end}}
|
||||
Restart=always
|
||||
RestartSec=120
|
||||
EnvironmentFile=-/etc/sysconfig/{{.Name}}
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`
|
|
@ -0,0 +1,252 @@
|
|||
// Copyright 2015 Daniel Theophanes.
|
||||
// Use of this source code is governed by a zlib-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
type sysv struct {
|
||||
i Interface
|
||||
*Config
|
||||
}
|
||||
|
||||
func newSystemVService(i Interface, c *Config) (Service, error) {
|
||||
s := &sysv{
|
||||
i: i,
|
||||
Config: c,
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *sysv) String() string {
|
||||
if len(s.DisplayName) > 0 {
|
||||
return s.DisplayName
|
||||
}
|
||||
return s.Name
|
||||
}
|
||||
|
||||
var errNoUserServiceSystemV = errors.New("User services are not supported on SystemV.")
|
||||
|
||||
func (s *sysv) configPath() (cp string, err error) {
|
||||
if s.Option.bool(optionUserService, optionUserServiceDefault) {
|
||||
err = errNoUserServiceSystemV
|
||||
return
|
||||
}
|
||||
cp = "/etc/init.d/" + s.Config.Name
|
||||
return
|
||||
}
|
||||
func (s *sysv) template() *template.Template {
|
||||
return template.Must(template.New("").Funcs(tf).Parse(sysvScript))
|
||||
}
|
||||
|
||||
func (s *sysv) Install() error {
|
||||
confPath, err := s.configPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = os.Stat(confPath)
|
||||
if err == nil {
|
||||
return fmt.Errorf("Init already exists: %s", confPath)
|
||||
}
|
||||
|
||||
f, err := os.Create(confPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
path, err := s.execPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var to = &struct {
|
||||
*Config
|
||||
Path string
|
||||
}{
|
||||
s.Config,
|
||||
path,
|
||||
}
|
||||
|
||||
err = s.template().Execute(f, to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = os.Chmod(confPath, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, i := range [...]string{"2", "3", "4", "5"} {
|
||||
if err = os.Symlink(confPath, "/etc/rc"+i+".d/S50"+s.Name); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
for _, i := range [...]string{"0", "1", "6"} {
|
||||
if err = os.Symlink(confPath, "/etc/rc"+i+".d/K02"+s.Name); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sysv) Uninstall() error {
|
||||
cp, err := s.configPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Remove(cp); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sysv) Logger(errs chan<- error) (Logger, error) {
|
||||
if system.Interactive() {
|
||||
return ConsoleLogger, nil
|
||||
}
|
||||
return s.SystemLogger(errs)
|
||||
}
|
||||
func (s *sysv) SystemLogger(errs chan<- error) (Logger, error) {
|
||||
return newSysLogger(s.Name, errs)
|
||||
}
|
||||
|
||||
func (s *sysv) Run() (err error) {
|
||||
err = s.i.Start(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.Option.funcSingle(optionRunWait, func() {
|
||||
var sigChan = make(chan os.Signal, 3)
|
||||
signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt)
|
||||
<-sigChan
|
||||
})()
|
||||
|
||||
return s.i.Stop(s)
|
||||
}
|
||||
|
||||
func (s *sysv) Start() error {
|
||||
return run("service", s.Name, "start")
|
||||
}
|
||||
|
||||
func (s *sysv) Stop() error {
|
||||
return run("service", s.Name, "stop")
|
||||
}
|
||||
|
||||
func (s *sysv) Restart() error {
|
||||
err := s.Stop()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
return s.Start()
|
||||
}
|
||||
|
||||
const sysvScript = `#!/bin/sh
|
||||
# For RedHat and cousins:
|
||||
# chkconfig: - 99 01
|
||||
# description: {{.Description}}
|
||||
# processname: {{.Path}}
|
||||
|
||||
### BEGIN INIT INFO
|
||||
# Provides: {{.Path}}
|
||||
# Required-Start:
|
||||
# Required-Stop:
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: {{.DisplayName}}
|
||||
# Description: {{.Description}}
|
||||
### END INIT INFO
|
||||
|
||||
cmd="{{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}"
|
||||
|
||||
name=$(basename $(readlink -f $0))
|
||||
pid_file="/var/run/$name.pid"
|
||||
stdout_log="/var/log/$name.log"
|
||||
stderr_log="/var/log/$name.err"
|
||||
|
||||
[ -e /etc/sysconfig/$name ] && . /etc/sysconfig/$name
|
||||
|
||||
get_pid() {
|
||||
cat "$pid_file"
|
||||
}
|
||||
|
||||
is_running() {
|
||||
[ -f "$pid_file" ] && ps $(get_pid) > /dev/null 2>&1
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
if is_running; then
|
||||
echo "Already started"
|
||||
else
|
||||
echo "Starting $name"
|
||||
{{if .WorkingDirectory}}cd '{{.WorkingDirectory}}'{{end}}
|
||||
$cmd >> "$stdout_log" 2>> "$stderr_log" &
|
||||
echo $! > "$pid_file"
|
||||
if ! is_running; then
|
||||
echo "Unable to start, see $stdout_log and $stderr_log"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
stop)
|
||||
if is_running; then
|
||||
echo -n "Stopping $name.."
|
||||
kill $(get_pid)
|
||||
for i in {1..10}
|
||||
do
|
||||
if ! is_running; then
|
||||
break
|
||||
fi
|
||||
echo -n "."
|
||||
sleep 1
|
||||
done
|
||||
echo
|
||||
if is_running; then
|
||||
echo "Not stopped; may still be shutting down or shutdown may have failed"
|
||||
exit 1
|
||||
else
|
||||
echo "Stopped"
|
||||
if [ -f "$pid_file" ]; then
|
||||
rm "$pid_file"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "Not running"
|
||||
fi
|
||||
;;
|
||||
restart)
|
||||
$0 stop
|
||||
if is_running; then
|
||||
echo "Unable to stop, will not attempt to start"
|
||||
exit 1
|
||||
fi
|
||||
$0 start
|
||||
;;
|
||||
status)
|
||||
if is_running; then
|
||||
echo "Running"
|
||||
else
|
||||
echo "Stopped"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart|status}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
`
|
|
@ -0,0 +1,57 @@
|
|||
// Copyright 2015 Daniel Theophanes.
|
||||
// Use of this source code is governed by a zlib-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package service_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
)
|
||||
|
||||
func TestRunInterrupt(t *testing.T) {
|
||||
p := &program{}
|
||||
sc := &service.Config{
|
||||
Name: "go_service_test",
|
||||
}
|
||||
s, err := service.New(p, sc)
|
||||
if err != nil {
|
||||
t.Fatalf("New err: %s", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-time.After(1 * time.Second)
|
||||
interruptProcess(t)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for i := 0; i < 25 && p.numStopped == 0; i++ {
|
||||
<-time.After(200 * time.Millisecond)
|
||||
}
|
||||
if p.numStopped == 0 {
|
||||
t.Fatal("Run() hasn't been stopped")
|
||||
}
|
||||
}()
|
||||
|
||||
if err = s.Run(); err != nil {
|
||||
t.Fatalf("Run() err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
type program struct {
|
||||
numStopped int
|
||||
}
|
||||
|
||||
func (p *program) Start(s service.Service) error {
|
||||
go p.run()
|
||||
return nil
|
||||
}
|
||||
func (p *program) run() {
|
||||
// Do work here
|
||||
}
|
||||
func (p *program) Stop(s service.Service) error {
|
||||
p.numStopped++
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
// Copyright 2015 Daniel Theophanes.
|
||||
// Use of this source code is governed by a zlib-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build linux darwin
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log/syslog"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func newSysLogger(name string, errs chan<- error) (Logger, error) {
|
||||
w, err := syslog.New(syslog.LOG_INFO, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sysLogger{w, errs}, nil
|
||||
}
|
||||
|
||||
type sysLogger struct {
|
||||
*syslog.Writer
|
||||
errs chan<- error
|
||||
}
|
||||
|
||||
func (s sysLogger) send(err error) error {
|
||||
if err != nil && s.errs != nil {
|
||||
s.errs <- err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s sysLogger) Error(v ...interface{}) error {
|
||||
return s.send(s.Writer.Err(fmt.Sprint(v...)))
|
||||
}
|
||||
func (s sysLogger) Warning(v ...interface{}) error {
|
||||
return s.send(s.Writer.Warning(fmt.Sprint(v...)))
|
||||
}
|
||||
func (s sysLogger) Info(v ...interface{}) error {
|
||||
return s.send(s.Writer.Info(fmt.Sprint(v...)))
|
||||
}
|
||||
func (s sysLogger) Errorf(format string, a ...interface{}) error {
|
||||
return s.send(s.Writer.Err(fmt.Sprintf(format, a...)))
|
||||
}
|
||||
func (s sysLogger) Warningf(format string, a ...interface{}) error {
|
||||
return s.send(s.Writer.Warning(fmt.Sprintf(format, a...)))
|
||||
}
|
||||
func (s sysLogger) Infof(format string, a ...interface{}) error {
|
||||
return s.send(s.Writer.Info(fmt.Sprintf(format, a...)))
|
||||
}
|
||||
|
||||
func run(command string, arguments ...string) error {
|
||||
cmd := exec.Command(command, arguments...)
|
||||
|
||||
// Connect pipe to read Stderr
|
||||
stderr, err := cmd.StderrPipe()
|
||||
|
||||
if err != nil {
|
||||
// Failed to connect pipe
|
||||
return fmt.Errorf("%q failed to connect stderr pipe: %v", command, err)
|
||||
}
|
||||
|
||||
// Do not use cmd.Run()
|
||||
if err := cmd.Start(); err != nil {
|
||||
// Problem while copying stdin, stdout, or stderr
|
||||
return fmt.Errorf("%q failed: %v", command, err)
|
||||
}
|
||||
|
||||
// Zero exit status
|
||||
// Darwin: launchctl can fail with a zero exit status,
|
||||
// so check for emtpy stderr
|
||||
if command == "launchctl" {
|
||||
slurp, _ := ioutil.ReadAll(stderr)
|
||||
if len(slurp) > 0 {
|
||||
return fmt.Errorf("%q failed with stderr: %s", command, slurp)
|
||||
}
|
||||
}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
// Command didn't exit with a zero exit status.
|
||||
return fmt.Errorf("%q failed: %v", command, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,181 @@
|
|||
// Copyright 2015 Daniel Theophanes.
|
||||
// Use of this source code is governed by a zlib-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
func isUpstart() bool {
|
||||
if _, err := os.Stat("/sbin/upstart-udev-bridge"); err == nil {
|
||||
return true
|
||||
}
|
||||
if _, err := os.Stat("/sbin/init"); err == nil {
|
||||
if out, err := exec.Command("/sbin/init", "--version").Output(); err == nil {
|
||||
if strings.Contains(string(out), "init (upstart") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type upstart struct {
|
||||
i Interface
|
||||
*Config
|
||||
}
|
||||
|
||||
func newUpstartService(i Interface, c *Config) (Service, error) {
|
||||
s := &upstart{
|
||||
i: i,
|
||||
Config: c,
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *upstart) String() string {
|
||||
if len(s.DisplayName) > 0 {
|
||||
return s.DisplayName
|
||||
}
|
||||
return s.Name
|
||||
}
|
||||
|
||||
// Upstart has some support for user services in graphical sessions.
|
||||
// Due to the mix of actual support for user services over versions, just don't bother.
|
||||
// Upstart will be replaced by systemd in most cases anyway.
|
||||
var errNoUserServiceUpstart = errors.New("User services are not supported on Upstart.")
|
||||
|
||||
func (s *upstart) configPath() (cp string, err error) {
|
||||
if s.Option.bool(optionUserService, optionUserServiceDefault) {
|
||||
err = errNoUserServiceUpstart
|
||||
return
|
||||
}
|
||||
cp = "/etc/init/" + s.Config.Name + ".conf"
|
||||
return
|
||||
}
|
||||
func (s *upstart) template() *template.Template {
|
||||
return template.Must(template.New("").Funcs(tf).Parse(upstartScript))
|
||||
}
|
||||
|
||||
func (s *upstart) Install() error {
|
||||
confPath, err := s.configPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = os.Stat(confPath)
|
||||
if err == nil {
|
||||
return fmt.Errorf("Init already exists: %s", confPath)
|
||||
}
|
||||
|
||||
f, err := os.Create(confPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
path, err := s.execPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var to = &struct {
|
||||
*Config
|
||||
Path string
|
||||
}{
|
||||
s.Config,
|
||||
path,
|
||||
}
|
||||
|
||||
return s.template().Execute(f, to)
|
||||
}
|
||||
|
||||
func (s *upstart) Uninstall() error {
|
||||
cp, err := s.configPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Remove(cp); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *upstart) Logger(errs chan<- error) (Logger, error) {
|
||||
if system.Interactive() {
|
||||
return ConsoleLogger, nil
|
||||
}
|
||||
return s.SystemLogger(errs)
|
||||
}
|
||||
func (s *upstart) SystemLogger(errs chan<- error) (Logger, error) {
|
||||
return newSysLogger(s.Name, errs)
|
||||
}
|
||||
|
||||
func (s *upstart) Run() (err error) {
|
||||
err = s.i.Start(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.Option.funcSingle(optionRunWait, func() {
|
||||
var sigChan = make(chan os.Signal, 3)
|
||||
signal.Notify(sigChan, os.Interrupt, os.Kill)
|
||||
<-sigChan
|
||||
})()
|
||||
|
||||
return s.i.Stop(s)
|
||||
}
|
||||
|
||||
func (s *upstart) Start() error {
|
||||
return run("initctl", "start", s.Name)
|
||||
}
|
||||
|
||||
func (s *upstart) Stop() error {
|
||||
return run("initctl", "stop", s.Name)
|
||||
}
|
||||
|
||||
func (s *upstart) Restart() error {
|
||||
err := s.Stop()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
return s.Start()
|
||||
}
|
||||
|
||||
// The upstart script should stop with an INT or the Go runtime will terminate
|
||||
// the program before the Stop handler can run.
|
||||
const upstartScript = `# {{.Description}}
|
||||
|
||||
{{if .DisplayName}}description "{{.DisplayName}}"{{end}}
|
||||
|
||||
kill signal INT
|
||||
{{if .ChRoot}}chroot {{.ChRoot}}{{end}}
|
||||
{{if .WorkingDirectory}}chdir {{.WorkingDirectory}}{{end}}
|
||||
start on filesystem or runlevel [2345]
|
||||
stop on runlevel [!2345]
|
||||
|
||||
{{if .UserName}}setuid {{.UserName}}{{end}}
|
||||
|
||||
respawn
|
||||
respawn limit 10 5
|
||||
umask 022
|
||||
|
||||
console none
|
||||
|
||||
pre-start script
|
||||
test -x {{.Path}} || { stop; exit 0; }
|
||||
end script
|
||||
|
||||
# Start
|
||||
exec {{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}
|
||||
`
|
|
@ -0,0 +1,389 @@
|
|||
// Copyright 2015 Daniel Theophanes.
|
||||
// Use of this source code is governed by a zlib-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows/registry"
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"golang.org/x/sys/windows/svc/eventlog"
|
||||
"golang.org/x/sys/windows/svc/mgr"
|
||||
)
|
||||
|
||||
const version = "windows-service"
|
||||
|
||||
type windowsService struct {
|
||||
i Interface
|
||||
*Config
|
||||
|
||||
errSync sync.Mutex
|
||||
stopStartErr error
|
||||
}
|
||||
|
||||
// WindowsLogger allows using windows specific logging methods.
|
||||
type WindowsLogger struct {
|
||||
ev *eventlog.Log
|
||||
errs chan<- error
|
||||
}
|
||||
|
||||
type windowsSystem struct{}
|
||||
|
||||
func (windowsSystem) String() string {
|
||||
return version
|
||||
}
|
||||
func (windowsSystem) Detect() bool {
|
||||
return true
|
||||
}
|
||||
func (windowsSystem) Interactive() bool {
|
||||
return interactive
|
||||
}
|
||||
func (windowsSystem) New(i Interface, c *Config) (Service, error) {
|
||||
ws := &windowsService{
|
||||
i: i,
|
||||
Config: c,
|
||||
}
|
||||
return ws, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
ChooseSystem(windowsSystem{})
|
||||
}
|
||||
|
||||
func (l WindowsLogger) send(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if l.errs != nil {
|
||||
l.errs <- err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Error logs an error message.
|
||||
func (l WindowsLogger) Error(v ...interface{}) error {
|
||||
return l.send(l.ev.Error(3, fmt.Sprint(v...)))
|
||||
}
|
||||
|
||||
// Warning logs an warning message.
|
||||
func (l WindowsLogger) Warning(v ...interface{}) error {
|
||||
return l.send(l.ev.Warning(2, fmt.Sprint(v...)))
|
||||
}
|
||||
|
||||
// Info logs an info message.
|
||||
func (l WindowsLogger) Info(v ...interface{}) error {
|
||||
return l.send(l.ev.Info(1, fmt.Sprint(v...)))
|
||||
}
|
||||
|
||||
// Errorf logs an error message.
|
||||
func (l WindowsLogger) Errorf(format string, a ...interface{}) error {
|
||||
return l.send(l.ev.Error(3, fmt.Sprintf(format, a...)))
|
||||
}
|
||||
|
||||
// Warningf logs an warning message.
|
||||
func (l WindowsLogger) Warningf(format string, a ...interface{}) error {
|
||||
return l.send(l.ev.Warning(2, fmt.Sprintf(format, a...)))
|
||||
}
|
||||
|
||||
// Infof logs an info message.
|
||||
func (l WindowsLogger) Infof(format string, a ...interface{}) error {
|
||||
return l.send(l.ev.Info(1, fmt.Sprintf(format, a...)))
|
||||
}
|
||||
|
||||
// NError logs an error message and an event ID.
|
||||
func (l WindowsLogger) NError(eventID uint32, v ...interface{}) error {
|
||||
return l.send(l.ev.Error(eventID, fmt.Sprint(v...)))
|
||||
}
|
||||
|
||||
// NWarning logs an warning message and an event ID.
|
||||
func (l WindowsLogger) NWarning(eventID uint32, v ...interface{}) error {
|
||||
return l.send(l.ev.Warning(eventID, fmt.Sprint(v...)))
|
||||
}
|
||||
|
||||
// NInfo logs an info message and an event ID.
|
||||
func (l WindowsLogger) NInfo(eventID uint32, v ...interface{}) error {
|
||||
return l.send(l.ev.Info(eventID, fmt.Sprint(v...)))
|
||||
}
|
||||
|
||||
// NErrorf logs an error message and an event ID.
|
||||
func (l WindowsLogger) NErrorf(eventID uint32, format string, a ...interface{}) error {
|
||||
return l.send(l.ev.Error(eventID, fmt.Sprintf(format, a...)))
|
||||
}
|
||||
|
||||
// NWarningf logs an warning message and an event ID.
|
||||
func (l WindowsLogger) NWarningf(eventID uint32, format string, a ...interface{}) error {
|
||||
return l.send(l.ev.Warning(eventID, fmt.Sprintf(format, a...)))
|
||||
}
|
||||
|
||||
// NInfof logs an info message and an event ID.
|
||||
func (l WindowsLogger) NInfof(eventID uint32, format string, a ...interface{}) error {
|
||||
return l.send(l.ev.Info(eventID, fmt.Sprintf(format, a...)))
|
||||
}
|
||||
|
||||
var interactive = false
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
interactive, err = svc.IsAnInteractiveSession()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *windowsService) String() string {
|
||||
if len(ws.DisplayName) > 0 {
|
||||
return ws.DisplayName
|
||||
}
|
||||
return ws.Name
|
||||
}
|
||||
|
||||
func (ws *windowsService) setError(err error) {
|
||||
ws.errSync.Lock()
|
||||
defer ws.errSync.Unlock()
|
||||
ws.stopStartErr = err
|
||||
}
|
||||
func (ws *windowsService) getError() error {
|
||||
ws.errSync.Lock()
|
||||
defer ws.errSync.Unlock()
|
||||
return ws.stopStartErr
|
||||
}
|
||||
|
||||
func (ws *windowsService) 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}
|
||||
|
||||
if err := ws.i.Start(ws); err != nil {
|
||||
ws.setError(err)
|
||||
return true, 1
|
||||
}
|
||||
|
||||
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
||||
loop:
|
||||
for {
|
||||
c := <-r
|
||||
switch c.Cmd {
|
||||
case svc.Interrogate:
|
||||
changes <- c.CurrentStatus
|
||||
case svc.Stop, svc.Shutdown:
|
||||
changes <- svc.Status{State: svc.StopPending}
|
||||
if err := ws.i.Stop(ws); err != nil {
|
||||
ws.setError(err)
|
||||
return true, 2
|
||||
}
|
||||
break loop
|
||||
default:
|
||||
continue loop
|
||||
}
|
||||
}
|
||||
|
||||
return false, 0
|
||||
}
|
||||
|
||||
func (ws *windowsService) Install() error {
|
||||
exepath, err := ws.execPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer m.Disconnect()
|
||||
s, err := m.OpenService(ws.Name)
|
||||
if err == nil {
|
||||
s.Close()
|
||||
return fmt.Errorf("service %s already exists", ws.Name)
|
||||
}
|
||||
s, err = m.CreateService(ws.Name, exepath, mgr.Config{
|
||||
DisplayName: ws.DisplayName,
|
||||
Description: ws.Description,
|
||||
StartType: mgr.StartAutomatic,
|
||||
ServiceStartName: ws.UserName,
|
||||
Password: ws.Option.string("Password", ""),
|
||||
Dependencies: ws.Dependencies,
|
||||
}, ws.Arguments...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.Close()
|
||||
err = eventlog.InstallAsEventCreate(ws.Name, eventlog.Error|eventlog.Warning|eventlog.Info)
|
||||
if err != nil {
|
||||
s.Delete()
|
||||
return fmt.Errorf("InstallAsEventCreate() failed: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ws *windowsService) Uninstall() error {
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer m.Disconnect()
|
||||
s, err := m.OpenService(ws.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("service %s is not installed", ws.Name)
|
||||
}
|
||||
defer s.Close()
|
||||
err = s.Delete()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = eventlog.Remove(ws.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("RemoveEventLogSource() failed: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ws *windowsService) Run() error {
|
||||
ws.setError(nil)
|
||||
if !interactive {
|
||||
// Return error messages from start and stop routines
|
||||
// that get executed in the Execute method.
|
||||
// Guarded with a mutex as it may run a different thread
|
||||
// (callback from windows).
|
||||
runErr := svc.Run(ws.Name, ws)
|
||||
startStopErr := ws.getError()
|
||||
if startStopErr != nil {
|
||||
return startStopErr
|
||||
}
|
||||
if runErr != nil {
|
||||
return runErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
err := ws.i.Start(ws)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sigChan := make(chan os.Signal)
|
||||
|
||||
signal.Notify(sigChan, os.Interrupt, os.Kill)
|
||||
|
||||
<-sigChan
|
||||
|
||||
return ws.i.Stop(ws)
|
||||
}
|
||||
|
||||
func (ws *windowsService) Start() error {
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer m.Disconnect()
|
||||
|
||||
s, err := m.OpenService(ws.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.Close()
|
||||
return s.Start()
|
||||
}
|
||||
|
||||
func (ws *windowsService) Stop() error {
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer m.Disconnect()
|
||||
|
||||
s, err := m.OpenService(ws.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
return ws.stopWait(s)
|
||||
}
|
||||
|
||||
func (ws *windowsService) Restart() error {
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer m.Disconnect()
|
||||
|
||||
s, err := m.OpenService(ws.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
err = ws.stopWait(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.Start()
|
||||
}
|
||||
|
||||
func (ws *windowsService) stopWait(s *mgr.Service) error {
|
||||
// First stop the service. Then wait for the service to
|
||||
// actually stop before starting it.
|
||||
status, err := s.Control(svc.Stop)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
timeDuration := time.Millisecond * 50
|
||||
|
||||
timeout := time.After(getStopTimeout() + (timeDuration * 2))
|
||||
tick := time.NewTicker(timeDuration)
|
||||
defer tick.Stop()
|
||||
|
||||
for status.State != svc.Stopped {
|
||||
select {
|
||||
case <-tick.C:
|
||||
status, err = s.Query()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case <-timeout:
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getStopTimeout fetches the time before windows will kill the service.
|
||||
func getStopTimeout() time.Duration {
|
||||
// For default and paths see https://support.microsoft.com/en-us/kb/146092
|
||||
defaultTimeout := time.Millisecond * 20000
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control`, registry.READ)
|
||||
if err != nil {
|
||||
return defaultTimeout
|
||||
}
|
||||
sv, _, err := key.GetStringValue("WaitToKillServiceTimeout")
|
||||
if err != nil {
|
||||
return defaultTimeout
|
||||
}
|
||||
v, err := strconv.Atoi(sv)
|
||||
if err != nil {
|
||||
return defaultTimeout
|
||||
}
|
||||
return time.Millisecond * time.Duration(v)
|
||||
}
|
||||
|
||||
func (ws *windowsService) Logger(errs chan<- error) (Logger, error) {
|
||||
if interactive {
|
||||
return ConsoleLogger, nil
|
||||
}
|
||||
return ws.SystemLogger(errs)
|
||||
}
|
||||
func (ws *windowsService) SystemLogger(errs chan<- error) (Logger, error) {
|
||||
el, err := eventlog.Open(ws.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return WindowsLogger{el, errs}, nil
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright 2015 Daniel Theophanes.
|
||||
// Use of this source code is governed by a zlib-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTimeout(t *testing.T) {
|
||||
stopSpan := getStopTimeout()
|
||||
t.Log("Max Stop Duration", stopSpan)
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright 2016 Lawrence Woodman <lwoodman@vlifesystems.com>
|
||||
// Use of this source code is governed by a zlib-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris
|
||||
|
||||
package service_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func interruptProcess(t *testing.T) {
|
||||
pid := os.Getpid()
|
||||
p, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
t.Fatalf("FindProcess: %s", err)
|
||||
}
|
||||
if err := p.Signal(os.Interrupt); err != nil {
|
||||
t.Fatalf("Signal: %s", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright 2016 Lawrence Woodman <lwoodman@vlifesystems.com>
|
||||
// Use of this source code is governed by a zlib-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package service_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func interruptProcess(t *testing.T) {
|
||||
dll, err := syscall.LoadDLL("kernel32.dll")
|
||||
if err != nil {
|
||||
t.Fatalf("LoadDLL(\"kernel32.dll\") err: %s", err)
|
||||
}
|
||||
p, err := dll.FindProc("GenerateConsoleCtrlEvent")
|
||||
if err != nil {
|
||||
t.Fatalf("FindProc(\"GenerateConsoleCtrlEvent\") err: %s", err)
|
||||
}
|
||||
// Send the CTRL_BREAK_EVENT to a console process group that shares
|
||||
// the console associated with the calling process.
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms683155(v=vs.85).aspx
|
||||
pid := os.Getpid()
|
||||
r1, _, err := p.Call(syscall.CTRL_BREAK_EVENT, uintptr(pid))
|
||||
if r1 == 0 {
|
||||
t.Fatalf("Call(CTRL_BREAK_EVENT, %d) err: %s", pid, err)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue