-
Notifications
You must be signed in to change notification settings - Fork 1
/
gohup.go
212 lines (175 loc) · 5.12 KB
/
gohup.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
package gohup
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"
stats "github.com/guillermo/go.procstat"
ps "github.com/mitchellh/go-ps"
)
// Status of the process
type Status string
// StatusRunning is returned when the process is Running
const StatusRunning Status = "StatusRunning"
// StatusStopped is returned when the process is Stopped
const StatusStopped Status = "StatusStopped"
// StatusError is returned when it is not possible to determine the status of a process
const StatusError Status = "StatusError"
// StatusNotFound is returned when the pid does not exist
const StatusNotFound Status = "StatusNotFound"
// Options to be used when starting a process
type Options struct {
// Path of the process to start
Path string
// Arguments to pass when starting the process
Args []string
// Environments to pass when starting the process
Env []string
// Directory to start the process in, default is the current directory
Dir string
// File to write the running process ID to, if blank a temporary file
// will be created.
Pidfile string
// File to write output from the started command to, if blank logs
// will be disgarded
Logfile string
}
// Process defines an interface which defines methods
// for managing a child process.
type Process interface {
// Start a process in the background
Start(options Options) (int, string, error)
// Stop the current running process
Stop(pidfile string, signal syscall.Signal) error
// Return the status of the currently running process
QueryStatus(pidfile string) (Status, error)
}
// LocalProcess is the implementation of the Process interface.
type LocalProcess struct {
options Options
}
// Start a local process in the background with the given options
// if the process does not start an error is returned.
// Start returns the process ID and the pidfile for the running process.
// If a process can not be started then an error will be returned.
func (l *LocalProcess) Start(options Options) (int, string, error) {
// Use the current directory unless Dir has been specified
if options.Dir == "" {
cwd, err := os.Getwd()
if err != nil {
return -1, "", err
}
options.Dir = cwd
}
cmd := exec.Command(options.Path, options.Args...)
cmd.Dir = options.Dir
cmd.Env = options.Env
cmd.SysProcAttr = SetSysProcAttr()
// create a logfile and redirect std error and std out
if options.Logfile != "" {
f, err := os.Create(options.Logfile)
if err != nil {
return -1, "", fmt.Errorf("Unable to open log file: %s", err)
}
cmd.Stderr = f
cmd.Stdout = f
}
// start the process
err := cmd.Start()
if err != nil {
return -1, "", err
}
pid := cmd.Process.Pid
cmd.Process.Release()
// If no pidfile is specified create a pid file in the
// temporary directory
if options.Pidfile == "" {
options.Pidfile = filepath.Join(os.TempDir(), fmt.Sprintf("%d.%s", pid, "pid"))
}
// write the pid file
err = l.writePidFile(pid, options.Pidfile)
if err != nil {
return -1, "", err
}
return pid, options.Pidfile, nil
}
// Stop the process referenced by the PID in the given file.
func (l *LocalProcess) Stop(pidfile string) error {
pid, err := l.readPidFile(pidfile)
if err != nil {
return err
}
p, err := os.FindProcess(pid)
if err != nil {
return err
}
// kill all the processes in the process group
Kill(p)
err = os.Remove(pidfile)
if err != nil {
return err
}
return nil
}
// QueryStatus of the backgrounded process referenced by the PID in the given file.
//
// If the process is not running StatusStopped, and a nil error will be returned.
// If the process is running StatusRunning, and a nil error will be returned.
// If it is not possible to query the status of the process or if the pidfile
// is not readable. StatusError, and an error will be returned.
func (l *LocalProcess) QueryStatus(pidfile string) (Status, error) {
pid, err := l.readPidFile(pidfile)
if err != nil {
return StatusNotFound, nil
}
p, err := ps.FindProcess(pid)
if err != nil {
return StatusError, err
}
if p == nil {
return StatusStopped, nil
}
if runtime.GOOS == "linux" {
// check the process is Zombie on Linux
stats := stats.Stat{Pid: pid}
err = stats.Update()
if err != nil {
return StatusError, err
}
if stats.State == 90 {
return StatusStopped, nil
}
} else if runtime.GOOS == "darwin" {
// check the process is Zombie on Darwin
output, _ := exec.Command("ps", fmt.Sprintf("%d", p.Pid())).CombinedOutput()
if strings.Contains(string(output), "<defunct>") {
return StatusStopped, nil
}
}
return StatusRunning, nil
}
func (l *LocalProcess) writePidFile(pid int, pidfile string) error {
f, err := os.Create(pidfile)
if err != nil {
return fmt.Errorf("unable to create pid file: %s", err)
}
defer f.Close()
f.WriteString(fmt.Sprintf("%d", pid))
return nil
}
func (l *LocalProcess) readPidFile(pidfile string) (int, error) {
d, err := ioutil.ReadFile(pidfile)
if err != nil {
return -1, fmt.Errorf("error reading file: %s", err)
}
pid, err := strconv.ParseInt(string(d), 10, 64)
if err != nil {
return -1, fmt.Errorf("unable to cast pid to integer: %s", err)
}
return int(pid), nil
}