forked from kevadesu/forgejo
Graceful: Cancel Process on monitor pages & HammerTime (#9213)
* Graceful: Create callbacks to with contexts * Graceful: Say when Gitea is completely finished * Graceful: Git and Process within HammerTime Force all git commands to terminate at HammerTime Force all process commands to terminate at HammerTime Move almost all git processes to run as git Commands * Graceful: Always Hammer after Shutdown * ProcessManager: Add cancel functionality * Fix tests * Make sure that process.Manager.Kill() cancels * Make threadsafe access to Processes and remove own unused Kill * Remove cmd from the process manager as it is no longer used * the default context is the correct context * get rid of double till
This commit is contained in:
parent
8f8c250ddb
commit
60c5339042
21 changed files with 536 additions and 198 deletions
|
@ -12,6 +12,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
@ -24,14 +25,17 @@ var (
|
|||
// ErrExecTimeout represent a timeout error
|
||||
ErrExecTimeout = errors.New("Process execution timeout")
|
||||
manager *Manager
|
||||
|
||||
// DefaultContext is the default context to run processing commands in
|
||||
DefaultContext = context.Background()
|
||||
)
|
||||
|
||||
// Process represents a working process inherit from Gogs.
|
||||
// Process represents a working process inheriting from Gitea.
|
||||
type Process struct {
|
||||
PID int64 // Process ID, not system one.
|
||||
Description string
|
||||
Start time.Time
|
||||
Cmd *exec.Cmd
|
||||
Cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// Manager knows about all processes and counts PIDs.
|
||||
|
@ -39,28 +43,28 @@ type Manager struct {
|
|||
mutex sync.Mutex
|
||||
|
||||
counter int64
|
||||
Processes map[int64]*Process
|
||||
processes map[int64]*Process
|
||||
}
|
||||
|
||||
// GetManager returns a Manager and initializes one as singleton if there's none yet
|
||||
func GetManager() *Manager {
|
||||
if manager == nil {
|
||||
manager = &Manager{
|
||||
Processes: make(map[int64]*Process),
|
||||
processes: make(map[int64]*Process),
|
||||
}
|
||||
}
|
||||
return manager
|
||||
}
|
||||
|
||||
// Add a process to the ProcessManager and returns its PID.
|
||||
func (pm *Manager) Add(description string, cmd *exec.Cmd) int64 {
|
||||
func (pm *Manager) Add(description string, cancel context.CancelFunc) int64 {
|
||||
pm.mutex.Lock()
|
||||
pid := pm.counter + 1
|
||||
pm.Processes[pid] = &Process{
|
||||
pm.processes[pid] = &Process{
|
||||
PID: pid,
|
||||
Description: description,
|
||||
Start: time.Now(),
|
||||
Cmd: cmd,
|
||||
Cancel: cancel,
|
||||
}
|
||||
pm.counter = pid
|
||||
pm.mutex.Unlock()
|
||||
|
@ -71,10 +75,32 @@ func (pm *Manager) Add(description string, cmd *exec.Cmd) int64 {
|
|||
// Remove a process from the ProcessManager.
|
||||
func (pm *Manager) Remove(pid int64) {
|
||||
pm.mutex.Lock()
|
||||
delete(pm.Processes, pid)
|
||||
delete(pm.processes, pid)
|
||||
pm.mutex.Unlock()
|
||||
}
|
||||
|
||||
// Cancel a process in the ProcessManager.
|
||||
func (pm *Manager) Cancel(pid int64) {
|
||||
pm.mutex.Lock()
|
||||
process, ok := pm.processes[pid]
|
||||
pm.mutex.Unlock()
|
||||
if ok {
|
||||
process.Cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// Processes gets the processes in a thread safe manner
|
||||
func (pm *Manager) Processes() []*Process {
|
||||
pm.mutex.Lock()
|
||||
processes := make([]*Process, 0, len(pm.processes))
|
||||
for _, process := range pm.processes {
|
||||
processes = append(processes, process)
|
||||
}
|
||||
pm.mutex.Unlock()
|
||||
sort.Sort(processList(processes))
|
||||
return processes
|
||||
}
|
||||
|
||||
// Exec a command and use the default timeout.
|
||||
func (pm *Manager) Exec(desc, cmdName string, args ...string) (string, string, error) {
|
||||
return pm.ExecDir(-1, "", desc, cmdName, args...)
|
||||
|
@ -110,7 +136,7 @@ func (pm *Manager) ExecDirEnvStdIn(timeout time.Duration, dir, desc string, env
|
|||
stdOut := new(bytes.Buffer)
|
||||
stdErr := new(bytes.Buffer)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
ctx, cancel := context.WithTimeout(DefaultContext, timeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, cmdName, args...)
|
||||
|
@ -126,7 +152,7 @@ func (pm *Manager) ExecDirEnvStdIn(timeout time.Duration, dir, desc string, env
|
|||
return "", "", err
|
||||
}
|
||||
|
||||
pid := pm.Add(desc, cmd)
|
||||
pid := pm.Add(desc, cancel)
|
||||
err := cmd.Wait()
|
||||
pm.Remove(pid)
|
||||
|
||||
|
@ -137,21 +163,16 @@ func (pm *Manager) ExecDirEnvStdIn(timeout time.Duration, dir, desc string, env
|
|||
return stdOut.String(), stdErr.String(), err
|
||||
}
|
||||
|
||||
// Kill and remove a process from list.
|
||||
func (pm *Manager) Kill(pid int64) error {
|
||||
if proc, exists := pm.Processes[pid]; exists {
|
||||
pm.mutex.Lock()
|
||||
if proc.Cmd != nil &&
|
||||
proc.Cmd.Process != nil &&
|
||||
proc.Cmd.ProcessState != nil &&
|
||||
!proc.Cmd.ProcessState.Exited() {
|
||||
if err := proc.Cmd.Process.Kill(); err != nil {
|
||||
return fmt.Errorf("failed to kill process(%d/%s): %v", pid, proc.Description, err)
|
||||
}
|
||||
}
|
||||
delete(pm.Processes, pid)
|
||||
pm.mutex.Unlock()
|
||||
}
|
||||
type processList []*Process
|
||||
|
||||
return nil
|
||||
func (l processList) Len() int {
|
||||
return len(l)
|
||||
}
|
||||
|
||||
func (l processList) Less(i, j int) bool {
|
||||
return l[i].PID < l[j].PID
|
||||
}
|
||||
|
||||
func (l processList) Swap(i, j int) {
|
||||
l[i], l[j] = l[j], l[i]
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package process
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -9,27 +9,42 @@ import (
|
|||
)
|
||||
|
||||
func TestManager_Add(t *testing.T) {
|
||||
pm := Manager{Processes: make(map[int64]*Process)}
|
||||
pm := Manager{processes: make(map[int64]*Process)}
|
||||
|
||||
pid := pm.Add("foo", exec.Command("foo"))
|
||||
pid := pm.Add("foo", nil)
|
||||
assert.Equal(t, int64(1), pid, "expected to get pid 1 got %d", pid)
|
||||
|
||||
pid = pm.Add("bar", exec.Command("bar"))
|
||||
pid = pm.Add("bar", nil)
|
||||
assert.Equal(t, int64(2), pid, "expected to get pid 2 got %d", pid)
|
||||
}
|
||||
|
||||
func TestManager_Remove(t *testing.T) {
|
||||
pm := Manager{Processes: make(map[int64]*Process)}
|
||||
func TestManager_Cancel(t *testing.T) {
|
||||
pm := Manager{processes: make(map[int64]*Process)}
|
||||
|
||||
pid1 := pm.Add("foo", exec.Command("foo"))
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
pid := pm.Add("foo", cancel)
|
||||
|
||||
pm.Cancel(pid)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
default:
|
||||
assert.Fail(t, "Cancel should cancel the provided context")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManager_Remove(t *testing.T) {
|
||||
pm := Manager{processes: make(map[int64]*Process)}
|
||||
|
||||
pid1 := pm.Add("foo", nil)
|
||||
assert.Equal(t, int64(1), pid1, "expected to get pid 1 got %d", pid1)
|
||||
|
||||
pid2 := pm.Add("bar", exec.Command("bar"))
|
||||
pid2 := pm.Add("bar", nil)
|
||||
assert.Equal(t, int64(2), pid2, "expected to get pid 2 got %d", pid2)
|
||||
|
||||
pm.Remove(pid2)
|
||||
|
||||
_, exists := pm.Processes[pid2]
|
||||
_, exists := pm.processes[pid2]
|
||||
assert.False(t, exists, "PID %d is in the list but shouldn't", pid2)
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue