├── README.md ├── process_test.go └── process.go /README.md: -------------------------------------------------------------------------------- 1 | # process 2 | process is a simple Go package for working with unix processes. 3 | 4 | For an example into what process is capable of doing, check out gobeat: https://github.com/radovskyb/gobeat. 5 | 6 | # Features 7 | 8 | - Find a process by `PID`. 9 | - Find a process by name. 10 | - Health check a process. 11 | - Start a new process. 12 | - Start a new process in a specified `tty`. 13 | - Other small features plus more to come... 14 | 15 | # Todo 16 | 17 | - Add `Kill` method. 18 | - Add `Restart` method. 19 | - Write more tests. 20 | -------------------------------------------------------------------------------- /process_test.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/exec" 8 | "strconv" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | var pid int 14 | var currentTty, cwd, cmd, fullCommand string 15 | var args []string 16 | 17 | func init() { 18 | pid = os.Getpid() 19 | 20 | ttyBytes, err := exec.Command("ps", "-o", "tty=", strconv.Itoa(pid)).Output() 21 | if err != nil { 22 | log.Fatalln(err) 23 | } 24 | currentTty = strings.TrimSpace(string(ttyBytes)) 25 | 26 | cmd = os.Args[0] 27 | 28 | cwd, err = os.Getwd() 29 | if err != nil { 30 | log.Fatalln(err) 31 | } 32 | 33 | args = os.Args[1:] 34 | 35 | if len(args) == 0 { 36 | fullCommand = cmd 37 | } else { 38 | fullCommand = fmt.Sprintf("%s %s", cmd, strings.Join(args, " ")) 39 | } 40 | } 41 | 42 | func TestFindByPid(t *testing.T) { 43 | proc, err := FindByPid(pid) 44 | if err != nil { 45 | log.Fatalln(err) 46 | } 47 | 48 | if proc.Tty != currentTty { 49 | t.Errorf("proc tty incorrect, expected %s found %s", 50 | currentTty, proc.Tty) 51 | } 52 | 53 | if proc.Cwd != cwd { 54 | t.Errorf("proc cwd incorrect, expected %s found %s", 55 | cwd, proc.Cwd) 56 | } 57 | 58 | for i, arg := range args { 59 | if proc.Args[i] != arg { 60 | t.Errorf("proc arg incorrect, expected %s found %s", 61 | arg, proc.Args[i]) 62 | } 63 | } 64 | 65 | if proc.Cmd != cmd { 66 | t.Errorf("proc cmd incorrect, expected %s found %s", 67 | cmd, proc.Cmd) 68 | } 69 | } 70 | 71 | func TestHealthCheck(t *testing.T) { 72 | // Start a new process that sleeps for 5 seconds. 73 | sleepCmd := exec.Command("sleep", "5") 74 | if err := sleepCmd.Start(); err != nil { 75 | t.Error(err) 76 | } 77 | 78 | // Create a new Process from the sleepCmd's process. 79 | proc, err := FindByPid(sleepCmd.Process.Pid) 80 | if err != nil { 81 | t.Error(err) 82 | } 83 | 84 | // HealthCheck the process and make sure it's running. 85 | if err := proc.HealthCheck(); err != nil { 86 | t.Error("expected process to be running") 87 | } 88 | 89 | // Stop the process. 90 | if err := proc.Release(); err != nil { 91 | t.Error(err) 92 | } 93 | 94 | // HealthCheck the process again and make sure it's stopped running. 95 | if err := proc.HealthCheck(); err == nil { 96 | t.Error("expected process to be stopped") 97 | } 98 | } 99 | 100 | func TestFullCommand(t *testing.T) { 101 | proc, err := FindByPid(pid) 102 | if err != nil { 103 | t.Error(err) 104 | } 105 | 106 | if proc.FullCommand() != fullCommand { 107 | t.Errorf("proc full command incorrect, expected %s, found %s", 108 | fullCommand, proc.FullCommand()) 109 | } 110 | } 111 | 112 | func TestFindProcess(t *testing.T) { 113 | proc := &Process{ 114 | Cmd: cmd, 115 | Args: args, 116 | Tty: currentTty, 117 | } 118 | 119 | if err := proc.FindProcess(); err != nil { 120 | t.Error(err) 121 | } 122 | 123 | if proc.Pid != pid { 124 | t.Errorf("proc pid is incorrect, expected %d, found %d", pid, proc.Pid) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /process.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "os" 9 | "os/exec" 10 | "strconv" 11 | "strings" 12 | "syscall" 13 | "unicode" 14 | "unsafe" 15 | ) 16 | 17 | var ( 18 | // ErrProcCommandEmpty is an error that occurs when calling FindProcess 19 | // for a Process and the Process's command is empty. 20 | ErrProcCommandEmpty = fmt.Errorf("error: process command is empty") 21 | 22 | // ErrProcNotRunning is an error that is returned when running a health check 23 | // for a process and the process is not running. 24 | ErrProcNotRunning = fmt.Errorf("error: process is not running") 25 | 26 | // ErrProcNotInTty is an error that occurs when trying to open a Process's 27 | // tty but the Process does not have it's tty value set. 28 | ErrProcNotInTty = fmt.Errorf("process is not in a tty") 29 | 30 | // ErrInvalidNumber is an error that occurs when the number scanned in 31 | // whilst searching for a ProcessByName is less than 0. 32 | ErrInvalidNumber = fmt.Errorf("please enter a valid number") 33 | ) 34 | 35 | // Process describes a unix process. 36 | // 37 | // The Process's Pid and the methods Kill(), Release(), Signal() 38 | // and Wait() are implemented by composition with os.Process. 39 | type Process struct { 40 | *os.Process 41 | Tty string 42 | Cwd string 43 | Cmd string 44 | Args []string 45 | } 46 | 47 | // String returns all of the process's relevant information as a string. 48 | func (p *Process) String() string { 49 | return fmt.Sprintf("[Pid]: %d\n"+ 50 | "[Command]: %s\n"+ 51 | "[Args]: %s\n"+ 52 | "[Cwd]: %v\n"+ 53 | "[Tty]: %s\n", 54 | p.Pid, 55 | p.Cmd, 56 | strings.Join(p.Args, ", "), 57 | p.Cwd, 58 | p.Tty, 59 | ) 60 | } 61 | 62 | // HealthCheck signals the process to see if it's still running. 63 | func (p *Process) HealthCheck() error { 64 | if err := p.Signal(syscall.Signal(0)); err != nil { 65 | return ErrProcNotRunning 66 | } 67 | return nil 68 | } 69 | 70 | // Start starts a process and notifies on the notify channel 71 | // when the process has been started. It uses stdin, stdout and 72 | // stderr for the command's stdin, stdout and stderr respectively. 73 | // 74 | // If the notify channel is nil, just return normally so the call doesn't block. 75 | func (p *Process) Start(detach bool, stdin io.Reader, stdout, stderr io.Writer, 76 | notify chan<- struct{}) error { 77 | // Create a new command to start the process with. 78 | c := exec.Command(p.Cmd, p.Args...) 79 | c.Stdin = stdin 80 | c.Stdout = stdout 81 | c.Stderr = stderr 82 | 83 | if p.InTty() { 84 | // Start the process in a different process group if detach is set to true. 85 | c.SysProcAttr = &syscall.SysProcAttr{Setpgid: detach} 86 | } else { 87 | // If process didn't start in a tty and detach is true, disconnect 88 | // process from any tty. 89 | c.SysProcAttr = &syscall.SysProcAttr{Setsid: detach} 90 | } 91 | 92 | // Start the command. 93 | if err := c.Start(); err != nil { 94 | return err 95 | } 96 | 97 | // Notify that the process has started if notify isn't nil. 98 | if notify != nil { 99 | notify <- struct{}{} 100 | } 101 | 102 | // Wait for the command to finish. 103 | return c.Wait() 104 | } 105 | 106 | // StartTty requires sudo to work. 107 | // 108 | // StartTty starts a process in a tty and notifies on the notify channel 109 | // when the process has been started. 110 | // 111 | // If the notify channel is nil, just return normally so the call doesn't block. 112 | // 113 | // The notify channel is here for consistency with the notify channel from 114 | // the Start method. 115 | func (p *Process) StartTty(ttyFd uintptr, notify chan<- struct{}) error { 116 | // Append a new line character to the full command so the command 117 | // actually executes. 118 | fullCommandNL := p.FullCommand() + "\n" 119 | 120 | // Write each byte from fullCommandNL to the tty instance. 121 | var eno syscall.Errno 122 | for _, b := range fullCommandNL { 123 | _, _, eno = syscall.Syscall(syscall.SYS_IOCTL, 124 | ttyFd, 125 | syscall.TIOCSTI, 126 | uintptr(unsafe.Pointer(&b)), 127 | ) 128 | if eno != 0 { 129 | return error(eno) 130 | } 131 | } 132 | 133 | // Get the new PID of the restarted process. 134 | if err := p.FindProcess(); err != nil { 135 | return err 136 | } 137 | 138 | // Notify that the process has started if notify isn't nil. 139 | if notify != nil { 140 | notify <- struct{}{} 141 | } 142 | 143 | return nil 144 | } 145 | 146 | // FindProcess finds and then sets a Process's process based 147 | // on it's command, it's command's arguments and it's tty. 148 | func (p *Process) FindProcess() error { 149 | if p.Process == nil { 150 | p.Process = &os.Process{} 151 | } 152 | 153 | if p.Cmd == "" { 154 | return ErrProcCommandEmpty 155 | } 156 | 157 | ps, err := exec.Command("ps", "-e").Output() 158 | if err != nil { 159 | return err 160 | } 161 | 162 | scanner := bufio.NewScanner(bytes.NewReader(ps)) 163 | for scanner.Scan() { 164 | line := scanner.Text() 165 | if strings.Contains(line, p.Cmd) && strings.Contains(line, p.Tty) { 166 | p.Pid, err = strconv.Atoi(strings.TrimSpace( 167 | strings.FieldsFunc(line, unicode.IsSpace)[0]), 168 | ) 169 | if err != nil { 170 | return err 171 | } 172 | } 173 | } 174 | if err := scanner.Err(); err != nil { 175 | return err 176 | } 177 | 178 | // Reset p.Process to the new process found from the new pid. 179 | p.Process, err = os.FindProcess(p.Pid) 180 | return err 181 | } 182 | 183 | // FullCommand returns a string containing the process's 184 | // cmd and any args that it has joined to it by a space. 185 | // 186 | // If there are no args, FullCommand returns just the cmd. 187 | func (p *Process) FullCommand() string { 188 | if len(p.Args) == 0 { 189 | return p.Cmd 190 | } 191 | return fmt.Sprintf("%s %s", p.Cmd, strings.Join(p.Args, " ")) 192 | } 193 | 194 | // InTty returns a true or false depending if p.Tty is ?? or 195 | // a value such as ttys001. 196 | func (p *Process) InTty() bool { 197 | return p.Tty != "??" 198 | } 199 | 200 | // OpenTty returns an opened file handle to the tty of the process. 201 | func (p *Process) OpenTty() (*os.File, error) { 202 | if !p.InTty() { 203 | return nil, ErrProcNotInTty 204 | } 205 | return os.Open("/dev/" + p.Tty) 206 | } 207 | 208 | // Chdir changes the current working directory to the processes cwd. 209 | func (p *Process) Chdir() error { 210 | return os.Chdir(p.Cwd) 211 | } 212 | 213 | // Find by name takes in a name and through a process of elimination by 214 | // prompting the user to select the correct process from a list, finds 215 | // and returns a process by it's name. 216 | // 217 | // FindByName writes the list of names to the specified stdout and then scans 218 | // the number for choosing the correct name from the specified stdin. 219 | func FindByName(stdout io.Writer, stdin io.Reader, name string) (*Process, error) { 220 | psOutput, err := exec.Command("ps", "-e").Output() 221 | if err != nil { 222 | return nil, err 223 | } 224 | lowercaseOutput := bytes.ToLower(psOutput) 225 | 226 | var names []string 227 | scanner := bufio.NewScanner(bytes.NewReader(lowercaseOutput)) 228 | for scanner.Scan() { 229 | line := scanner.Text() 230 | if strings.Contains(line, name) { 231 | names = append(names, line) 232 | } 233 | } 234 | if err := scanner.Err(); err != nil { 235 | return nil, err 236 | } 237 | 238 | // Display a list of all the found names. 239 | for i, name := range names { 240 | fmt.Printf("%d: %s\n", i, name) 241 | } 242 | 243 | procNumber := -1 244 | fmt.Fprintln(stdout, "\nWhich number above represents the correct process (enter the number):") 245 | fmt.Fscanf(stdin, "%d", &procNumber) 246 | 247 | if procNumber < 0 { 248 | return nil, ErrInvalidNumber 249 | } 250 | 251 | pid, err := strconv.Atoi(strings.TrimSpace( 252 | strings.FieldsFunc(names[procNumber], unicode.IsSpace)[0]), 253 | ) 254 | if err != nil { 255 | return nil, err 256 | } 257 | 258 | return FindByPid(pid) 259 | } 260 | 261 | // FindByPid finds and returns a process by it's pid. 262 | func FindByPid(pid int) (*Process, error) { 263 | proc := new(Process) 264 | 265 | var err error 266 | proc.Process, err = os.FindProcess(pid) 267 | if err != nil { 268 | return nil, err 269 | } 270 | 271 | pidStr := strconv.Itoa(proc.Pid) 272 | 273 | // Get the tty= and comm= result from ps. Extract the tty of the process from 274 | // the tty= result and use the comm= result to compare to the command= result 275 | // below, to extract the process's command args. 276 | // 277 | // ps -o tty=,comm= -p $PID 278 | pidCmd, err := exec.Command("ps", "-o", "tty=,comm=", pidStr).Output() 279 | if err != nil { 280 | return nil, err 281 | } 282 | 283 | // Split the tty and command parts from the result of the above ps command. 284 | psfields := strings.FieldsFunc(string(pidCmd), unicode.IsSpace) 285 | 286 | // Get the tty of the process. 287 | proc.Tty = psfields[0] 288 | 289 | // Get the proc's command. 290 | proc.Cmd = strings.Join(psfields[1:], " ") 291 | 292 | // Extract process's args. 293 | // 294 | // Get the ps command= string result. 295 | pidCommandEq, err := exec.Command("ps", "-o", "command=", pidStr).Output() 296 | if err != nil { 297 | return nil, err 298 | } 299 | 300 | // Split the command= string after the comm= string. 301 | split := strings.SplitAfter(string(pidCommandEq), proc.Cmd) 302 | 303 | // Set the process's args. 304 | proc.Args = strings.FieldsFunc(split[1], unicode.IsSpace) 305 | 306 | // Find folder of the process (cwd). 307 | // 308 | // lsof -p $PID 309 | lsofOutput, err := exec.Command("lsof", "-p", pidStr).Output() 310 | if err != nil { 311 | return nil, err 312 | } 313 | 314 | scanner := bufio.NewScanner(bytes.NewReader(lsofOutput)) 315 | for scanner.Scan() { 316 | words := strings.FieldsFunc(scanner.Text(), unicode.IsSpace) 317 | if words[3] == "cwd" { 318 | proc.Cwd = strings.TrimSpace(strings.Join(words[8:], " ")) 319 | } 320 | } 321 | if err := scanner.Err(); err != nil { 322 | return nil, err 323 | } 324 | 325 | return proc, nil 326 | } 327 | --------------------------------------------------------------------------------