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