github.com/cloudfoundry-attic/garden-linux@v0.333.2-candidate/process_tracker/process_tracker_test.go (about) 1 package process_tracker_test 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "os" 10 "os/exec" 11 "path" 12 "path/filepath" 13 14 . "github.com/onsi/ginkgo" 15 . "github.com/onsi/gomega" 16 "github.com/onsi/gomega/gbytes" 17 "github.com/pivotal-golang/lager/lagertest" 18 19 "github.com/cloudfoundry-incubator/garden" 20 "github.com/cloudfoundry-incubator/garden-linux/process_tracker" 21 "github.com/cloudfoundry/gunk/command_runner/linux_command_runner" 22 ) 23 24 var _ = Describe("Process tracker", func() { 25 var ( 26 testLogger *lagertest.TestLogger 27 processTracker process_tracker.ProcessTracker 28 tmpdir string 29 signaller process_tracker.Signaller 30 ) 31 32 BeforeEach(func() { 33 var err error 34 35 testLogger = lagertest.NewTestLogger("process-tracker-tests") 36 37 tmpdir, err = ioutil.TempDir("", "process-tracker-tests") 38 Expect(err).ToNot(HaveOccurred()) 39 40 err = os.MkdirAll(filepath.Join(tmpdir, "bin"), 0755) 41 Expect(err).ToNot(HaveOccurred()) 42 43 err = copyFile(iodaemonBin, filepath.Join(tmpdir, "bin", "iodaemon")) 44 Expect(err).ToNot(HaveOccurred()) 45 46 signaller = &process_tracker.LinkSignaller{} 47 48 processTracker = process_tracker.New(testLogger, tmpdir, linux_command_runner.New()) 49 }) 50 51 AfterEach(func() { 52 os.RemoveAll(tmpdir) 53 }) 54 55 Describe("Running processes", func() { 56 It("runs the process and returns its exit code", func() { 57 cmd := exec.Command("bash", "-c", "exit 42") 58 59 process, err := processTracker.Run("555", cmd, garden.ProcessIO{}, nil, signaller) 60 Expect(err).NotTo(HaveOccurred()) 61 62 status, err := process.Wait() 63 Expect(err).ToNot(HaveOccurred()) 64 Expect(status).To(Equal(42)) 65 }) 66 67 It("logs its progress to the given logger", func() { 68 69 cmd := exec.Command("bash", "-c", "exit 42") 70 71 process, err := processTracker.Run("555", cmd, garden.ProcessIO{}, nil, signaller) 72 Expect(err).NotTo(HaveOccurred()) 73 74 Eventually(testLogger).Should(gbytes.Say("run-spawning")) 75 76 status, err := process.Wait() 77 Expect(err).ToNot(HaveOccurred()) 78 Expect(status).To(Equal(42)) 79 80 }) 81 82 Describe("signalling a running process", func() { 83 var ( 84 process garden.Process 85 stdout *gbytes.Buffer 86 cmd *exec.Cmd 87 ) 88 89 JustBeforeEach(func() { 90 var err error 91 cmd = exec.Command(testPrintBin) 92 93 stdout = gbytes.NewBuffer() 94 process, err = processTracker.Run( 95 "2", cmd, 96 garden.ProcessIO{ 97 Stdout: io.MultiWriter(stdout, GinkgoWriter), 98 Stderr: GinkgoWriter, 99 }, nil, signaller) 100 Expect(err).NotTo(HaveOccurred()) 101 102 Eventually(stdout).Should(gbytes.Say("pid")) 103 }) 104 105 AfterEach(func() { 106 if cmd.ProcessState != nil && !cmd.ProcessState.Exited() { 107 cmd.Process.Signal(os.Kill) 108 } 109 }) 110 111 Context("when the signaller is an LinkSignaller", func() { 112 It("sends a kill message on the extra file descriptor", func(done Done) { 113 Expect(process.Signal(garden.SignalKill)).To(Succeed()) 114 Eventually(stdout).Should(gbytes.Say("Received: killed")) 115 close(done) 116 }, 2.0) 117 118 It("kills the process with a terminate signal", func(done Done) { 119 Expect(process.Signal(garden.SignalTerminate)).To(Succeed()) 120 Eventually(stdout).Should(gbytes.Say("Received: terminated")) 121 close(done) 122 }, 2.0) 123 124 Context("when an unsupported signal is sent", func() { 125 AfterEach(func() { 126 Expect(process.Signal(garden.SignalKill)).To(Succeed()) 127 }) 128 129 It("return error", func() { 130 Expect(process.Signal(garden.Signal(999))).To(MatchError(HaveSuffix("failed to send signal: unknown signal: 999"))) 131 }) 132 }) 133 }) 134 }) 135 136 It("streams the process's stdout and stderr", func() { 137 cmd := exec.Command( 138 "/bin/bash", 139 "-c", 140 "echo 'hi out' && echo 'hi err' >&2", 141 ) 142 143 stdout := gbytes.NewBuffer() 144 stderr := gbytes.NewBuffer() 145 146 _, err := processTracker.Run("40", cmd, garden.ProcessIO{ 147 Stdout: stdout, 148 Stderr: stderr, 149 }, nil, signaller) 150 Expect(err).NotTo(HaveOccurred()) 151 152 Eventually(stdout).Should(gbytes.Say("hi out\n")) 153 Eventually(stderr).Should(gbytes.Say("hi err\n")) 154 }) 155 156 It("streams input to the process", func() { 157 stdout := gbytes.NewBuffer() 158 159 _, err := processTracker.Run("50", exec.Command("cat"), garden.ProcessIO{ 160 Stdin: bytes.NewBufferString("stdin-line1\nstdin-line2\n"), 161 Stdout: stdout, 162 }, nil, signaller) 163 Expect(err).NotTo(HaveOccurred()) 164 165 Eventually(stdout).Should(gbytes.Say("stdin-line1\nstdin-line2\n")) 166 }) 167 168 Context("when there is an error reading the stdin stream", func() { 169 It("does not close the process's stdin", func() { 170 pipeR, pipeW := io.Pipe() 171 stdout := gbytes.NewBuffer() 172 173 process, err := processTracker.Run("60", exec.Command("cat"), garden.ProcessIO{ 174 Stdin: pipeR, 175 Stdout: stdout, 176 }, nil, signaller) 177 Expect(err).NotTo(HaveOccurred()) 178 179 pipeW.Write([]byte("Hello stdin!")) 180 Eventually(stdout).Should(gbytes.Say("Hello stdin!")) 181 182 pipeW.CloseWithError(errors.New("Failed")) 183 Consistently(stdout, 0.1).ShouldNot(gbytes.Say(".")) 184 185 pipeR, pipeW = io.Pipe() 186 processTracker.Attach(process.ID(), garden.ProcessIO{ 187 Stdin: pipeR, 188 }) 189 190 pipeW.Write([]byte("Hello again, stdin!")) 191 Eventually(stdout).Should(gbytes.Say("Hello again, stdin!")) 192 193 pipeW.Close() 194 exitStatus, err := process.Wait() 195 Expect(err).ToNot(HaveOccurred()) 196 Expect(exitStatus).To(Equal(0)) 197 }) 198 199 It("supports attaching more than once", func() { 200 pipeR, pipeW := io.Pipe() 201 stdout := gbytes.NewBuffer() 202 203 process, err := processTracker.Run("70", exec.Command("cat"), garden.ProcessIO{ 204 Stdin: pipeR, 205 Stdout: stdout, 206 }, nil, signaller) 207 Expect(err).NotTo(HaveOccurred()) 208 209 pipeW.Write([]byte("Hello stdin!")) 210 Eventually(stdout).Should(gbytes.Say("Hello stdin!")) 211 212 pipeW.CloseWithError(errors.New("Failed")) 213 Consistently(stdout, 0.1).ShouldNot(gbytes.Say(".")) 214 215 pipeR, pipeW = io.Pipe() 216 _, err = processTracker.Attach(process.ID(), garden.ProcessIO{ 217 Stdin: pipeR, 218 }) 219 Expect(err).ToNot(HaveOccurred()) 220 221 pipeW.Write([]byte("Hello again, stdin!")) 222 Eventually(stdout).Should(gbytes.Say("Hello again, stdin!")) 223 224 pipeR, pipeW = io.Pipe() 225 226 _, err = processTracker.Attach(process.ID(), garden.ProcessIO{ 227 Stdin: pipeR, 228 }) 229 Expect(err).ToNot(HaveOccurred()) 230 231 pipeW.Write([]byte("Hello again again, stdin!")) 232 Eventually(stdout, "1s").Should(gbytes.Say("Hello again again, stdin!")) 233 234 pipeW.Close() 235 Expect(process.Wait()).To(Equal(0)) 236 }) 237 }) 238 239 Context("with a tty", func() { 240 It("forwards TTY signals to the process", func() { 241 cmd := exec.Command("/bin/bash", "-c", ` 242 trap "stty size; exit 123" SIGWINCH 243 stty size 244 read 245 `) 246 247 stdout := gbytes.NewBuffer() 248 249 process, err := processTracker.Run("90", cmd, garden.ProcessIO{ 250 Stdout: stdout, 251 }, &garden.TTYSpec{ 252 WindowSize: &garden.WindowSize{ 253 Columns: 95, 254 Rows: 13, 255 }, 256 }, signaller) 257 Expect(err).NotTo(HaveOccurred()) 258 259 Eventually(stdout).Should(gbytes.Say("13 95")) 260 261 process.SetTTY(garden.TTYSpec{ 262 WindowSize: &garden.WindowSize{ 263 Columns: 101, 264 Rows: 27, 265 }, 266 }) 267 268 Eventually(stdout).Should(gbytes.Say("27 101")) 269 Expect(process.Wait()).To(Equal(123)) 270 }) 271 272 Describe("when a window size is not specified", func() { 273 It("picks a default window size", func() { 274 cmd := exec.Command("/bin/bash", "-c", ` 275 stty size 276 `) 277 278 stdout := gbytes.NewBuffer() 279 280 _, err := processTracker.Run("100", cmd, garden.ProcessIO{ 281 Stdout: stdout, 282 }, &garden.TTYSpec{}, signaller) 283 Expect(err).NotTo(HaveOccurred()) 284 285 Eventually(stdout).Should(gbytes.Say("24 80")) 286 }) 287 }) 288 }) 289 290 Context("when spawning fails", func() { 291 Context("because the binary doesn't exist", func() { 292 It("returns the error", func() { 293 _, err := processTracker.Run("200", exec.Command("/bin/does-not-exist"), garden.ProcessIO{}, nil, signaller) 294 Expect(err).To(MatchError(ContainSubstring("executable /bin/does-not-exist not found"))) 295 }) 296 }) 297 298 Context("because the binary is corrupted", func() { 299 var ( 300 corruptedBinary string 301 tmpDir string 302 ) 303 304 BeforeEach(func() { 305 var err error 306 tmpDir, err = ioutil.TempDir("", "") 307 Expect(err).NotTo(HaveOccurred()) 308 309 corruptedBinary = path.Join(tmpDir, "corrupted") 310 Expect(ioutil.WriteFile(corruptedBinary, []byte("fail-to-exec"), 0755)).To(Succeed()) 311 }) 312 313 AfterEach(func() { 314 Expect(os.RemoveAll(tmpDir)).To(Succeed()) 315 }) 316 317 It("returns the error", func() { 318 _, err := processTracker.Run("200", exec.Command(corruptedBinary), garden.ProcessIO{}, nil, signaller) 319 Expect(err).To(MatchError(ContainSubstring(fmt.Sprintf("executable %s failed to start", corruptedBinary)))) 320 }) 321 }) 322 }) 323 }) 324 325 Describe("Restoring processes", func() { 326 It("tracks the restored process", func() { 327 processTracker.Restore("2", signaller) 328 329 activeProcesses := processTracker.ActiveProcesses() 330 Expect(activeProcesses).To(HaveLen(1)) 331 Expect(activeProcesses[0].ID()).To(Equal("2")) 332 }) 333 }) 334 335 Describe("Attaching to running processes", func() { 336 It("streams stdout, stdin, and stderr", func() { 337 cmd := exec.Command("bash", "-c", ` 338 stuff=$(cat) 339 echo "hi stdout" $stuff 340 echo "hi stderr" $stuff >&2 341 `) 342 343 process, err := processTracker.Run("855", cmd, garden.ProcessIO{}, nil, signaller) 344 Expect(err).NotTo(HaveOccurred()) 345 346 stdout := gbytes.NewBuffer() 347 stderr := gbytes.NewBuffer() 348 349 process, err = processTracker.Attach(process.ID(), garden.ProcessIO{ 350 Stdin: bytes.NewBufferString("this-is-stdin"), 351 Stdout: stdout, 352 Stderr: stderr, 353 }) 354 Expect(err).NotTo(HaveOccurred()) 355 356 Eventually(stdout).Should(gbytes.Say("hi stdout this-is-stdin")) 357 Eventually(stderr).Should(gbytes.Say("hi stderr this-is-stdin")) 358 }) 359 }) 360 361 Describe("Listing active process IDs", func() { 362 It("includes running process IDs", func() { 363 stdin1, stdinWriter1 := io.Pipe() 364 stdin2, stdinWriter2 := io.Pipe() 365 366 Expect(processTracker.ActiveProcesses()).To(BeEmpty()) 367 368 process1, err := processTracker.Run("9955", exec.Command("cat"), garden.ProcessIO{ 369 Stdin: stdin1, 370 }, nil, signaller) 371 Expect(err).ToNot(HaveOccurred()) 372 373 Eventually(processTracker.ActiveProcesses).Should(ConsistOf(process1)) 374 375 process2, err := processTracker.Run("9956", exec.Command("cat"), garden.ProcessIO{ 376 Stdin: stdin2, 377 }, nil, signaller) 378 Expect(err).ToNot(HaveOccurred()) 379 380 Eventually(processTracker.ActiveProcesses).Should(ConsistOf(process1, process2)) 381 382 stdinWriter1.Close() 383 Eventually(processTracker.ActiveProcesses).Should(ConsistOf(process2)) 384 385 stdinWriter2.Close() 386 Eventually(processTracker.ActiveProcesses).Should(BeEmpty()) 387 }) 388 }) 389 }) 390 391 func copyFile(src, dst string) error { 392 s, err := os.Open(src) 393 if err != nil { 394 return err 395 } 396 397 defer s.Close() 398 399 d, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE, 0755) 400 if err != nil { 401 return err 402 } 403 404 _, err = io.Copy(d, s) 405 if err != nil { 406 d.Close() 407 return err 408 } 409 410 return d.Close() 411 }