github.com/Comcast/plax@v0.8.32/dsl/process.go (about) 1 /* 2 * Copyright 2021 Comcast Cable Communications Management, LLC 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 * 16 * SPDX-License-Identifier: Apache-2.0 17 */ 18 19 package dsl 20 21 import ( 22 "bufio" 23 "fmt" 24 "io" 25 "log" 26 "os/exec" 27 "strings" 28 "syscall" 29 ) 30 31 // Process represents an external process run from a test. 32 type Process struct { 33 // Name is an opaque string used is reports about this 34 // Process. 35 Name string `json:"name" yaml:"name"` 36 37 // Command is the name of the program. 38 // 39 // Subject to expansion. 40 Command string `json:"command" yaml:"command"` 41 42 // Args is the list of command-line arguments for the program. 43 // 44 // Subject to expansion. 45 Args []string `json:"args" yaml:"args"` 46 47 // ToDo: Environment, dir. 48 49 cmd *exec.Cmd 50 51 Stdout chan string `json:"-"` 52 Stderr chan string `json:"-"` 53 Stdin chan string `json:"-"` 54 ExitCode chan int `json:"-"` 55 56 // ctl is only used to terminate goroutines when the Process 57 // is terminated. 58 ctl chan bool 59 } 60 61 // Substitute the bindings into the Process 62 func (p *Process) Substitute(ctx *Ctx, bs *Bindings) (*Process, error) { 63 cmd, err := bs.StringSub(ctx, p.Command) 64 if err != nil { 65 return nil, err 66 } 67 args := make([]string, len(p.Args)) 68 for i, arg := range p.Args { 69 s, err := bs.StringSub(ctx, arg) 70 if err != nil { 71 return nil, err 72 } 73 args[i] = s 74 } 75 return &Process{ 76 Name: p.Name, 77 Command: cmd, 78 Args: args, 79 }, nil 80 } 81 82 // TrimEOL is a utility function that removes the last (if any) 83 // newline character(s). 84 // 85 // This function does not trim more than one newline. 86 func TrimEOL(s string) string { 87 s = strings.TrimSuffix(s, "\n") 88 return strings.TrimSuffix(s, "\r") 89 } 90 91 // Start starts the program, which runs in the background (until the 92 // test is complete). 93 // 94 // Stderr and stdout are logged via ctx.Logf. 95 func (p *Process) Start(ctx *Ctx) error { 96 97 p.Stdin = make(chan string) 98 p.Stderr = make(chan string) 99 p.Stdout = make(chan string) 100 p.ctl = make(chan bool) 101 p.ExitCode = make(chan int) 102 103 p.cmd = exec.Command(p.Command, p.Args...) 104 105 inPipe, err := p.cmd.StdinPipe() 106 if err != nil { 107 return fmt.Errorf("Process %s Run error on StdinPipe: %s", p.Name, err) 108 } 109 110 go func() { 111 for { 112 select { 113 case <-ctx.Done(): 114 return 115 case line := <-p.Stdin: 116 line = TrimEOL(line) + "\n" 117 if _, err = io.WriteString(inPipe, line); err != nil { 118 log.Printf("Warning: Process Write: %s", err) 119 } 120 } 121 } 122 }() 123 124 errPipe, err := p.cmd.StderrPipe() 125 if err != nil { 126 return fmt.Errorf("Process %s Run error on StderrPipe: %s", p.Name, err) 127 } 128 129 go func() { 130 in := bufio.NewScanner(errPipe) 131 for in.Scan() { 132 line := in.Text() 133 ctx.Logf("Process %s stderr line: %s\n", p.Name, line) 134 select { 135 case <-ctx.Done(): 136 return 137 case <-p.ctl: 138 return 139 case p.Stderr <- line: 140 } 141 } 142 if err := in.Err(); err != nil { 143 ctx.Logf("Process %s stderr error %s", p.Name, err) 144 } 145 }() 146 147 outPipe, err := p.cmd.StdoutPipe() 148 if err != nil { 149 return fmt.Errorf("Process %s Run error on StdoutPipe: %s", p.Name, err) 150 } 151 152 go func() { 153 in := bufio.NewScanner(outPipe) 154 for in.Scan() { 155 line := in.Text() 156 ctx.Logf("Process %s stdout line: %s\n", p.Name, line) 157 select { 158 case <-ctx.Done(): 159 return 160 case <-p.ctl: 161 return 162 case p.Stdout <- line: 163 } 164 } 165 if err := in.Err(); err != nil { 166 ctx.Logf("Process %s stdout error %s", p.Name, err) 167 } 168 }() 169 170 if err := p.cmd.Start(); err != nil { 171 ctx.Logf("Process %s error on start: %s", p.Name, err) 172 return err 173 } 174 175 go func() { 176 if err := p.cmd.Wait(); err != nil { 177 ctx.Logf("Process %s error on wait: %s", p.Name, err) 178 } 179 180 p.ExitCode <- p.cmd.ProcessState.ExitCode() 181 }() 182 183 return nil 184 } 185 186 // Term sends a SIGTERM to the process. 187 func (p *Process) Term(ctx *Ctx) error { 188 close(p.ctl) 189 p.cmd.Process.Signal(syscall.SIGTERM) 190 return nil 191 } 192 193 // Kill kills the Process. 194 func (p *Process) Kill(ctx *Ctx) error { 195 ctx.Logf("Process %s killed", p.Name) 196 return p.cmd.Process.Kill() 197 }