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.

143 lines
3.7 KiB

  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package process
  5. import (
  6. "bytes"
  7. "context"
  8. "errors"
  9. "fmt"
  10. "os/exec"
  11. "sync"
  12. "time"
  13. )
  14. // TODO: This packages still uses a singleton for the Manager.
  15. // Once there's a decent web framework and dependencies are passed around like they should,
  16. // then we delete the singleton.
  17. var (
  18. // ErrExecTimeout represent a timeout error
  19. ErrExecTimeout = errors.New("Process execution timeout")
  20. manager *Manager
  21. )
  22. // Process represents a working process inherit from Gogs.
  23. type Process struct {
  24. PID int64 // Process ID, not system one.
  25. Description string
  26. Start time.Time
  27. Cmd *exec.Cmd
  28. }
  29. // Manager knows about all processes and counts PIDs.
  30. type Manager struct {
  31. mutex sync.Mutex
  32. counter int64
  33. Processes map[int64]*Process
  34. }
  35. // GetManager returns a Manager and initializes one as singleton if there's none yet
  36. func GetManager() *Manager {
  37. if manager == nil {
  38. manager = &Manager{
  39. Processes: make(map[int64]*Process),
  40. }
  41. }
  42. return manager
  43. }
  44. // Add a process to the ProcessManager and returns its PID.
  45. func (pm *Manager) Add(description string, cmd *exec.Cmd) int64 {
  46. pm.mutex.Lock()
  47. pid := pm.counter + 1
  48. pm.Processes[pid] = &Process{
  49. PID: pid,
  50. Description: description,
  51. Start: time.Now(),
  52. Cmd: cmd,
  53. }
  54. pm.counter = pid
  55. pm.mutex.Unlock()
  56. return pid
  57. }
  58. // Remove a process from the ProcessManager.
  59. func (pm *Manager) Remove(pid int64) {
  60. pm.mutex.Lock()
  61. delete(pm.Processes, pid)
  62. pm.mutex.Unlock()
  63. }
  64. // Exec a command and use the default timeout.
  65. func (pm *Manager) Exec(desc, cmdName string, args ...string) (string, string, error) {
  66. return pm.ExecDir(-1, "", desc, cmdName, args...)
  67. }
  68. // ExecTimeout a command and use a specific timeout duration.
  69. func (pm *Manager) ExecTimeout(timeout time.Duration, desc, cmdName string, args ...string) (string, string, error) {
  70. return pm.ExecDir(timeout, "", desc, cmdName, args...)
  71. }
  72. // ExecDir a command and use the default timeout.
  73. func (pm *Manager) ExecDir(timeout time.Duration, dir, desc, cmdName string, args ...string) (string, string, error) {
  74. return pm.ExecDirEnv(timeout, dir, desc, nil, cmdName, args...)
  75. }
  76. // ExecDirEnv runs a command in given path and environment variables, and waits for its completion
  77. // up to the given timeout (or DefaultTimeout if -1 is given).
  78. // Returns its complete stdout and stderr
  79. // outputs and an error, if any (including timeout)
  80. func (pm *Manager) ExecDirEnv(timeout time.Duration, dir, desc string, env []string, cmdName string, args ...string) (string, string, error) {
  81. if timeout == -1 {
  82. timeout = 60 * time.Second
  83. }
  84. stdOut := new(bytes.Buffer)
  85. stdErr := new(bytes.Buffer)
  86. ctx, cancel := context.WithTimeout(context.Background(), timeout)
  87. defer cancel()
  88. cmd := exec.CommandContext(ctx, cmdName, args...)
  89. cmd.Dir = dir
  90. cmd.Env = env
  91. cmd.Stdout = stdOut
  92. cmd.Stderr = stdErr
  93. if err := cmd.Start(); err != nil {
  94. return "", "", err
  95. }
  96. pid := pm.Add(desc, cmd)
  97. err := cmd.Wait()
  98. pm.Remove(pid)
  99. if err != nil {
  100. err = fmt.Errorf("exec(%d:%s) failed: %v(%v) stdout: %v stderr: %v", pid, desc, err, ctx.Err(), stdOut, stdErr)
  101. }
  102. return stdOut.String(), stdErr.String(), err
  103. }
  104. // Kill and remove a process from list.
  105. func (pm *Manager) Kill(pid int64) error {
  106. if proc, exists := pm.Processes[pid]; exists {
  107. pm.mutex.Lock()
  108. if proc.Cmd != nil &&
  109. proc.Cmd.Process != nil &&
  110. proc.Cmd.ProcessState != nil &&
  111. !proc.Cmd.ProcessState.Exited() {
  112. if err := proc.Cmd.Process.Kill(); err != nil {
  113. return fmt.Errorf("failed to kill process(%d/%s): %v", pid, proc.Description, err)
  114. }
  115. }
  116. delete(pm.Processes, pid)
  117. pm.mutex.Unlock()
  118. }
  119. return nil
  120. }