You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

script.go 2.0KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. // Copyright (c) 2020 Shivaram Lingamneni
  2. // released under the MIT license
  3. package irc
  4. import (
  5. "bufio"
  6. "io"
  7. "os/exec"
  8. "syscall"
  9. "time"
  10. )
  11. // general-purpose scripting API for oragono "plugins"
  12. // invoke a command, send it a single newline-terminated string of bytes (typically JSON)
  13. // get back another newline-terminated string of bytes (or an error)
  14. // internal tupling of output and error for passing over a channel
  15. type scriptResponse struct {
  16. output []byte
  17. err error
  18. }
  19. func RunScript(command string, args []string, input []byte, timeout, killTimeout time.Duration) (output []byte, err error) {
  20. cmd := exec.Command(command, args...)
  21. stdin, err := cmd.StdinPipe()
  22. if err != nil {
  23. return
  24. }
  25. stdout, err := cmd.StdoutPipe()
  26. if err != nil {
  27. return
  28. }
  29. channel := make(chan scriptResponse, 1)
  30. err = cmd.Start()
  31. if err != nil {
  32. return
  33. }
  34. stdin.Write(input)
  35. stdin.Write([]byte{'\n'})
  36. // lots of potential race conditions here. we want to ensure that Wait()
  37. // will be called, and will return, on the other goroutine, no matter
  38. // where it is blocked. If it's blocked on ReadBytes(), we will kill it
  39. // (first with SIGTERM, then with SIGKILL) and ReadBytes will return
  40. // with EOF. If it's blocked on Wait(), then one of the kill signals
  41. // will succeed and unblock it.
  42. go processScriptOutput(cmd, stdout, channel)
  43. outputTimer := time.NewTimer(timeout)
  44. select {
  45. case response := <-channel:
  46. return response.output, response.err
  47. case <-outputTimer.C:
  48. }
  49. err = errTimedOut
  50. cmd.Process.Signal(syscall.SIGTERM)
  51. termTimer := time.NewTimer(killTimeout)
  52. select {
  53. case <-channel:
  54. return
  55. case <-termTimer.C:
  56. }
  57. cmd.Process.Kill()
  58. return
  59. }
  60. func processScriptOutput(cmd *exec.Cmd, stdout io.Reader, channel chan scriptResponse) {
  61. var response scriptResponse
  62. reader := bufio.NewReader(stdout)
  63. response.output, response.err = reader.ReadBytes('\n')
  64. // always call Wait() to ensure resource cleanup
  65. err := cmd.Wait()
  66. if err != nil {
  67. response.err = err
  68. }
  69. channel <- response
  70. }