aboutsummaryrefslogtreecommitdiffstats
path: root/modules/process
diff options
context:
space:
mode:
Diffstat (limited to 'modules/process')
-rw-r--r--modules/process/error.go26
-rw-r--r--modules/process/manager.go225
-rw-r--r--modules/process/manager_exec.go79
-rw-r--r--modules/process/manager_stacktraces.go355
-rw-r--r--modules/process/manager_test.go8
-rw-r--r--modules/process/process.go65
6 files changed, 551 insertions, 207 deletions
diff --git a/modules/process/error.go b/modules/process/error.go
new file mode 100644
index 0000000000..7a72bda40e
--- /dev/null
+++ b/modules/process/error.go
@@ -0,0 +1,26 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package process
+
+import "fmt"
+
+// Error is a wrapped error describing the error results of Process Execution
+type Error struct {
+ PID IDType
+ Description string
+ Err error
+ CtxErr error
+ Stdout string
+ Stderr string
+}
+
+func (err *Error) Error() string {
+ return fmt.Sprintf("exec(%s:%s) failed: %v(%v) stdout: %s stderr: %s", err.PID, err.Description, err.Err, err.CtxErr, err.Stdout, err.Stderr)
+}
+
+// Unwrap implements the unwrappable implicit interface for go1.13 Unwrap()
+func (err *Error) Unwrap() error {
+ return err.Err
+}
diff --git a/modules/process/manager.go b/modules/process/manager.go
index 26dd6d535f..5d7aee760f 100644
--- a/modules/process/manager.go
+++ b/modules/process/manager.go
@@ -6,13 +6,8 @@
package process
import (
- "bytes"
"context"
- "fmt"
- "io"
- "os/exec"
"runtime/pprof"
- "sort"
"strconv"
"sync"
"time"
@@ -30,6 +25,18 @@ var (
DefaultContext = context.Background()
)
+// DescriptionPProfLabel is a label set on goroutines that have a process attached
+const DescriptionPProfLabel = "process-description"
+
+// PIDPProfLabel is a label set on goroutines that have a process attached
+const PIDPProfLabel = "pid"
+
+// PPIDPProfLabel is a label set on goroutines that have a process attached
+const PPIDPProfLabel = "ppid"
+
+// ProcessTypePProfLabel is a label set on goroutines that have a process attached
+const ProcessTypePProfLabel = "process-type"
+
// IDType is a pid type
type IDType string
@@ -44,15 +51,15 @@ type Manager struct {
next int64
lastTime int64
- processes map[IDType]*Process
+ processMap map[IDType]*process
}
// GetManager returns a Manager and initializes one as singleton if there's none yet
func GetManager() *Manager {
managerInit.Do(func() {
manager = &Manager{
- processes: make(map[IDType]*Process),
- next: 1,
+ processMap: make(map[IDType]*process),
+ next: 1,
}
})
return manager
@@ -69,12 +76,25 @@ func GetManager() *Manager {
func (pm *Manager) AddContext(parent context.Context, description string) (ctx context.Context, cancel context.CancelFunc, finished FinishedFunc) {
ctx, cancel = context.WithCancel(parent)
- ctx, pid, finished := pm.Add(ctx, description, cancel)
+ ctx, _, finished = pm.Add(ctx, description, cancel, NormalProcessType, true)
- return &Context{
- Context: ctx,
- pid: pid,
- }, cancel, finished
+ return ctx, cancel, finished
+}
+
+// AddTypedContext creates a new context and adds it as a process. Once the process is finished, finished must be called
+// to remove the process from the process table. It should not be called until the process is finished but must always be called.
+//
+// cancel should be used to cancel the returned context, however it will not remove the process from the process table.
+// finished will cancel the returned context and remove it from the process table.
+//
+// Most processes will not need to use the cancel function but there will be cases whereby you want to cancel the process but not immediately remove it from the
+// process table.
+func (pm *Manager) AddTypedContext(parent context.Context, description, processType string, currentlyRunning bool) (ctx context.Context, cancel context.CancelFunc, finished FinishedFunc) {
+ ctx, cancel = context.WithCancel(parent)
+
+ ctx, _, finished = pm.Add(ctx, description, cancel, processType, currentlyRunning)
+
+ return ctx, cancel, finished
}
// AddContextTimeout creates a new context and add it as a process. Once the process is finished, finished must be called
@@ -90,52 +110,61 @@ func (pm *Manager) AddContextTimeout(parent context.Context, timeout time.Durati
// it's meaningless to use timeout <= 0, and it must be a bug! so we must panic here to tell developers to make the timeout correct
panic("the timeout must be greater than zero, otherwise the context will be cancelled immediately")
}
+
ctx, cancel = context.WithTimeout(parent, timeout)
- ctx, pid, finshed := pm.Add(ctx, description, cancel)
+ ctx, _, finshed = pm.Add(ctx, description, cancel, NormalProcessType, true)
- return &Context{
- Context: ctx,
- pid: pid,
- }, cancel, finshed
+ return ctx, cancel, finshed
}
// Add create a new process
-func (pm *Manager) Add(ctx context.Context, description string, cancel context.CancelFunc) (context.Context, IDType, FinishedFunc) {
+func (pm *Manager) Add(ctx context.Context, description string, cancel context.CancelFunc, processType string, currentlyRunning bool) (context.Context, IDType, FinishedFunc) {
parentPID := GetParentPID(ctx)
pm.mutex.Lock()
start, pid := pm.nextPID()
- parent := pm.processes[parentPID]
+ parent := pm.processMap[parentPID]
if parent == nil {
parentPID = ""
}
- process := &Process{
+ process := &process{
PID: pid,
ParentPID: parentPID,
Description: description,
Start: start,
Cancel: cancel,
+ Type: processType,
}
- finished := func() {
- cancel()
- pm.remove(process)
- pprof.SetGoroutineLabels(ctx)
+ var finished FinishedFunc
+ if currentlyRunning {
+ finished = func() {
+ cancel()
+ pm.remove(process)
+ pprof.SetGoroutineLabels(ctx)
+ }
+ } else {
+ finished = func() {
+ cancel()
+ pm.remove(process)
+ }
}
- if parent != nil {
- parent.AddChild(process)
- }
- pm.processes[pid] = process
+ pm.processMap[pid] = process
pm.mutex.Unlock()
- pprofCtx := pprof.WithLabels(ctx, pprof.Labels("process-description", description, "ppid", string(parentPID), "pid", string(pid)))
- pprof.SetGoroutineLabels(pprofCtx)
+ pprofCtx := pprof.WithLabels(ctx, pprof.Labels(DescriptionPProfLabel, description, PPIDPProfLabel, string(parentPID), PIDPProfLabel, string(pid), ProcessTypePProfLabel, processType))
+ if currentlyRunning {
+ pprof.SetGoroutineLabels(pprofCtx)
+ }
- return pprofCtx, pid, finished
+ return &Context{
+ Context: pprofCtx,
+ pid: pid,
+ }, pid, finished
}
// nextPID will return the next available PID. pm.mutex should already be locked.
@@ -160,142 +189,24 @@ func (pm *Manager) nextPID() (start time.Time, pid IDType) {
// Remove a process from the ProcessManager.
func (pm *Manager) Remove(pid IDType) {
pm.mutex.Lock()
- delete(pm.processes, pid)
+ delete(pm.processMap, pid)
pm.mutex.Unlock()
}
-func (pm *Manager) remove(process *Process) {
+func (pm *Manager) remove(process *process) {
pm.mutex.Lock()
- if p := pm.processes[process.PID]; p == process {
- delete(pm.processes, process.PID)
+ defer pm.mutex.Unlock()
+ if p := pm.processMap[process.PID]; p == process {
+ delete(pm.processMap, process.PID)
}
- parent := pm.processes[process.ParentPID]
- pm.mutex.Unlock()
-
- if parent == nil {
- return
- }
-
- parent.RemoveChild(process)
}
// Cancel a process in the ProcessManager.
func (pm *Manager) Cancel(pid IDType) {
pm.mutex.Lock()
- process, ok := pm.processes[pid]
+ process, ok := pm.processMap[pid]
pm.mutex.Unlock()
- if ok {
+ if ok && process.Type != SystemProcessType {
process.Cancel()
}
}
-
-// Processes gets the processes in a thread safe manner
-func (pm *Manager) Processes(onlyRoots bool) []*Process {
- pm.mutex.Lock()
- processes := make([]*Process, 0, len(pm.processes))
- if onlyRoots {
- for _, process := range pm.processes {
- if _, has := pm.processes[process.ParentPID]; !has {
- processes = append(processes, process)
- }
- }
- } else {
- for _, process := range pm.processes {
- processes = append(processes, process)
- }
- }
- pm.mutex.Unlock()
-
- sort.Slice(processes, func(i, j int) bool {
- left, right := processes[i], processes[j]
-
- return left.Start.Before(right.Start)
- })
-
- 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(DefaultContext, -1, "", desc, cmdName, args...)
-}
-
-// ExecTimeout a command and use a specific timeout duration.
-func (pm *Manager) ExecTimeout(timeout time.Duration, desc, cmdName string, args ...string) (string, string, error) {
- return pm.ExecDir(DefaultContext, timeout, "", desc, cmdName, args...)
-}
-
-// ExecDir a command and use the default timeout.
-func (pm *Manager) ExecDir(ctx context.Context, timeout time.Duration, dir, desc, cmdName string, args ...string) (string, string, error) {
- return pm.ExecDirEnv(ctx, timeout, dir, desc, nil, cmdName, args...)
-}
-
-// ExecDirEnv runs a command in given path and environment variables, and waits for its completion
-// up to the given timeout (or DefaultTimeout if -1 is given).
-// Returns its complete stdout and stderr
-// outputs and an error, if any (including timeout)
-func (pm *Manager) ExecDirEnv(ctx context.Context, timeout time.Duration, dir, desc string, env []string, cmdName string, args ...string) (string, string, error) {
- return pm.ExecDirEnvStdIn(ctx, timeout, dir, desc, env, nil, cmdName, args...)
-}
-
-// ExecDirEnvStdIn runs a command in given path and environment variables with provided stdIN, and waits for its completion
-// up to the given timeout (or DefaultTimeout if -1 is given).
-// Returns its complete stdout and stderr
-// outputs and an error, if any (including timeout)
-func (pm *Manager) ExecDirEnvStdIn(ctx context.Context, timeout time.Duration, dir, desc string, env []string, stdIn io.Reader, cmdName string, args ...string) (string, string, error) {
- if timeout <= 0 {
- timeout = 60 * time.Second
- }
-
- stdOut := new(bytes.Buffer)
- stdErr := new(bytes.Buffer)
-
- ctx, _, finished := pm.AddContextTimeout(ctx, timeout, desc)
- defer finished()
-
- cmd := exec.CommandContext(ctx, cmdName, args...)
- cmd.Dir = dir
- cmd.Env = env
- cmd.Stdout = stdOut
- cmd.Stderr = stdErr
- if stdIn != nil {
- cmd.Stdin = stdIn
- }
-
- if err := cmd.Start(); err != nil {
- return "", "", err
- }
-
- err := cmd.Wait()
- if err != nil {
- err = &Error{
- PID: GetPID(ctx),
- Description: desc,
- Err: err,
- CtxErr: ctx.Err(),
- Stdout: stdOut.String(),
- Stderr: stdErr.String(),
- }
- }
-
- return stdOut.String(), stdErr.String(), err
-}
-
-// Error is a wrapped error describing the error results of Process Execution
-type Error struct {
- PID IDType
- Description string
- Err error
- CtxErr error
- Stdout string
- Stderr string
-}
-
-func (err *Error) Error() string {
- return fmt.Sprintf("exec(%s:%s) failed: %v(%v) stdout: %s stderr: %s", err.PID, err.Description, err.Err, err.CtxErr, err.Stdout, err.Stderr)
-}
-
-// Unwrap implements the unwrappable implicit interface for go1.13 Unwrap()
-func (err *Error) Unwrap() error {
- return err.Err
-}
diff --git a/modules/process/manager_exec.go b/modules/process/manager_exec.go
new file mode 100644
index 0000000000..61ddae646f
--- /dev/null
+++ b/modules/process/manager_exec.go
@@ -0,0 +1,79 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package process
+
+import (
+ "bytes"
+ "context"
+ "io"
+ "os/exec"
+ "time"
+)
+
+// Exec a command and use the default timeout.
+func (pm *Manager) Exec(desc, cmdName string, args ...string) (string, string, error) {
+ return pm.ExecDir(DefaultContext, -1, "", desc, cmdName, args...)
+}
+
+// ExecTimeout a command and use a specific timeout duration.
+func (pm *Manager) ExecTimeout(timeout time.Duration, desc, cmdName string, args ...string) (string, string, error) {
+ return pm.ExecDir(DefaultContext, timeout, "", desc, cmdName, args...)
+}
+
+// ExecDir a command and use the default timeout.
+func (pm *Manager) ExecDir(ctx context.Context, timeout time.Duration, dir, desc, cmdName string, args ...string) (string, string, error) {
+ return pm.ExecDirEnv(ctx, timeout, dir, desc, nil, cmdName, args...)
+}
+
+// ExecDirEnv runs a command in given path and environment variables, and waits for its completion
+// up to the given timeout (or DefaultTimeout if -1 is given).
+// Returns its complete stdout and stderr
+// outputs and an error, if any (including timeout)
+func (pm *Manager) ExecDirEnv(ctx context.Context, timeout time.Duration, dir, desc string, env []string, cmdName string, args ...string) (string, string, error) {
+ return pm.ExecDirEnvStdIn(ctx, timeout, dir, desc, env, nil, cmdName, args...)
+}
+
+// ExecDirEnvStdIn runs a command in given path and environment variables with provided stdIN, and waits for its completion
+// up to the given timeout (or DefaultTimeout if timeout <= 0 is given).
+// Returns its complete stdout and stderr
+// outputs and an error, if any (including timeout)
+func (pm *Manager) ExecDirEnvStdIn(ctx context.Context, timeout time.Duration, dir, desc string, env []string, stdIn io.Reader, cmdName string, args ...string) (string, string, error) {
+ if timeout <= 0 {
+ timeout = 60 * time.Second
+ }
+
+ stdOut := new(bytes.Buffer)
+ stdErr := new(bytes.Buffer)
+
+ ctx, _, finished := pm.AddContextTimeout(ctx, timeout, desc)
+ defer finished()
+
+ cmd := exec.CommandContext(ctx, cmdName, args...)
+ cmd.Dir = dir
+ cmd.Env = env
+ cmd.Stdout = stdOut
+ cmd.Stderr = stdErr
+ if stdIn != nil {
+ cmd.Stdin = stdIn
+ }
+
+ if err := cmd.Start(); err != nil {
+ return "", "", err
+ }
+
+ err := cmd.Wait()
+ if err != nil {
+ err = &Error{
+ PID: GetPID(ctx),
+ Description: desc,
+ Err: err,
+ CtxErr: ctx.Err(),
+ Stdout: stdOut.String(),
+ Stderr: stdErr.String(),
+ }
+ }
+
+ return stdOut.String(), stdErr.String(), err
+}
diff --git a/modules/process/manager_stacktraces.go b/modules/process/manager_stacktraces.go
new file mode 100644
index 0000000000..fbe3374b87
--- /dev/null
+++ b/modules/process/manager_stacktraces.go
@@ -0,0 +1,355 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package process
+
+import (
+ "fmt"
+ "io"
+ "runtime/pprof"
+ "sort"
+ "time"
+
+ "github.com/google/pprof/profile"
+)
+
+// StackEntry is an entry on a stacktrace
+type StackEntry struct {
+ Function string
+ File string
+ Line int
+}
+
+// Label represents a pprof label assigned to goroutine stack
+type Label struct {
+ Name string
+ Value string
+}
+
+// Stack is a stacktrace relating to a goroutine. (Multiple goroutines may have the same stacktrace)
+type Stack struct {
+ Count int64 // Number of goroutines with this stack trace
+ Description string
+ Labels []*Label `json:",omitempty"`
+ Entry []*StackEntry `json:",omitempty"`
+}
+
+// A Process is a combined representation of a Process and a Stacktrace for the goroutines associated with it
+type Process struct {
+ PID IDType
+ ParentPID IDType
+ Description string
+ Start time.Time
+ Type string
+
+ Children []*Process `json:",omitempty"`
+ Stacks []*Stack `json:",omitempty"`
+}
+
+// Processes gets the processes in a thread safe manner
+func (pm *Manager) Processes(flat, noSystem bool) ([]*Process, int) {
+ pm.mutex.Lock()
+ processCount := len(pm.processMap)
+ processes := make([]*Process, 0, len(pm.processMap))
+ if flat {
+ for _, process := range pm.processMap {
+ if noSystem && process.Type == SystemProcessType {
+ continue
+ }
+ processes = append(processes, process.toProcess())
+ }
+ } else {
+ // We need our own processMap
+ processMap := map[IDType]*Process{}
+ for _, internalProcess := range pm.processMap {
+ process, ok := processMap[internalProcess.PID]
+ if !ok {
+ process = internalProcess.toProcess()
+ processMap[process.PID] = process
+ }
+
+ // Check its parent
+ if process.ParentPID == "" {
+ processes = append(processes, process)
+ continue
+ }
+
+ internalParentProcess, ok := pm.processMap[internalProcess.ParentPID]
+ if ok {
+ parentProcess, ok := processMap[process.ParentPID]
+ if !ok {
+ parentProcess = internalParentProcess.toProcess()
+ processMap[parentProcess.PID] = parentProcess
+ }
+ parentProcess.Children = append(parentProcess.Children, process)
+ continue
+ }
+
+ processes = append(processes, process)
+ }
+ }
+ pm.mutex.Unlock()
+
+ if !flat && noSystem {
+ for i := 0; i < len(processes); i++ {
+ process := processes[i]
+ if process.Type != SystemProcessType {
+ continue
+ }
+ processes[len(processes)-1], processes[i] = processes[i], processes[len(processes)-1]
+ processes = append(processes[:len(processes)-1], process.Children...)
+ i--
+ }
+ }
+
+ // Sort by process' start time. Oldest process appears first.
+ sort.Slice(processes, func(i, j int) bool {
+ left, right := processes[i], processes[j]
+
+ return left.Start.Before(right.Start)
+ })
+
+ return processes, processCount
+}
+
+// ProcessStacktraces gets the processes and stacktraces in a thread safe manner
+func (pm *Manager) ProcessStacktraces(flat, noSystem bool) ([]*Process, int, int64, error) {
+ var stacks *profile.Profile
+ var err error
+
+ // We cannot use the pm.ProcessMap here because we will release the mutex ...
+ processMap := map[IDType]*Process{}
+ processCount := 0
+
+ // Lock the manager
+ pm.mutex.Lock()
+ processCount = len(pm.processMap)
+
+ // Add a defer to unlock in case there is a panic
+ unlocked := false
+ defer func() {
+ if !unlocked {
+ pm.mutex.Unlock()
+ }
+ }()
+
+ processes := make([]*Process, 0, len(pm.processMap))
+ if flat {
+ for _, internalProcess := range pm.processMap {
+ process := internalProcess.toProcess()
+ processMap[process.PID] = process
+ if noSystem && internalProcess.Type == SystemProcessType {
+ continue
+ }
+ processes = append(processes, process)
+ }
+ } else {
+ for _, internalProcess := range pm.processMap {
+ process, ok := processMap[internalProcess.PID]
+ if !ok {
+ process = internalProcess.toProcess()
+ processMap[process.PID] = process
+ }
+
+ // Check its parent
+ if process.ParentPID == "" {
+ processes = append(processes, process)
+ continue
+ }
+
+ internalParentProcess, ok := pm.processMap[internalProcess.ParentPID]
+ if ok {
+ parentProcess, ok := processMap[process.ParentPID]
+ if !ok {
+ parentProcess = internalParentProcess.toProcess()
+ processMap[parentProcess.PID] = parentProcess
+ }
+ parentProcess.Children = append(parentProcess.Children, process)
+ continue
+ }
+
+ processes = append(processes, process)
+ }
+ }
+
+ // Now from within the lock we need to get the goroutines.
+ // Why? If we release the lock then between between filling the above map and getting
+ // the stacktraces another process could be created which would then look like a dead process below
+ reader, writer := io.Pipe()
+ defer reader.Close()
+ go func() {
+ err := pprof.Lookup("goroutine").WriteTo(writer, 0)
+ _ = writer.CloseWithError(err)
+ }()
+ stacks, err = profile.Parse(reader)
+ if err != nil {
+ return nil, 0, 0, err
+ }
+
+ // Unlock the mutex
+ pm.mutex.Unlock()
+ unlocked = true
+
+ goroutineCount := int64(0)
+
+ // Now walk through the "Sample" slice in the goroutines stack
+ for _, sample := range stacks.Sample {
+ // In the "goroutine" pprof profile each sample represents one or more goroutines
+ // with the same labels and stacktraces.
+
+ // We will represent each goroutine by a `Stack`
+ stack := &Stack{}
+
+ // Add the non-process associated labels from the goroutine sample to the Stack
+ for name, value := range sample.Label {
+ if name == DescriptionPProfLabel || name == PIDPProfLabel || (!flat && name == PPIDPProfLabel) || name == ProcessTypePProfLabel {
+ continue
+ }
+
+ // Labels from the "goroutine" pprof profile only have one value.
+ // This is because the underlying representation is a map[string]string
+ if len(value) != 1 {
+ // Unexpected...
+ return nil, 0, 0, fmt.Errorf("label: %s in goroutine stack with unexpected number of values: %v", name, value)
+ }
+
+ stack.Labels = append(stack.Labels, &Label{Name: name, Value: value[0]})
+ }
+
+ // The number of goroutines that this sample represents is the `stack.Value[0]`
+ stack.Count = sample.Value[0]
+ goroutineCount += stack.Count
+
+ // Now we want to associate this Stack with a Process.
+ var process *Process
+
+ // Try to get the PID from the goroutine labels
+ if pidvalue, ok := sample.Label[PIDPProfLabel]; ok && len(pidvalue) == 1 {
+ pid := IDType(pidvalue[0])
+
+ // Now try to get the process from our map
+ process, ok = processMap[pid]
+ if !ok && pid != "" {
+ // This means that no process has been found in the process map - but there was a process PID
+ // Therefore this goroutine belongs to a dead process and it has escaped control of the process as it
+ // should have died with the process context cancellation.
+
+ // We need to create a dead process holder for this process and label it appropriately
+
+ // get the parent PID
+ ppid := IDType("")
+ if value, ok := sample.Label[PPIDPProfLabel]; ok && len(value) == 1 {
+ ppid = IDType(value[0])
+ }
+
+ // format the description
+ description := "(dead process)"
+ if value, ok := sample.Label[DescriptionPProfLabel]; ok && len(value) == 1 {
+ description = value[0] + " " + description
+ }
+
+ // override the type of the process to "code" but add the old type as a label on the first stack
+ ptype := NoneProcessType
+ if value, ok := sample.Label[ProcessTypePProfLabel]; ok && len(value) == 1 {
+ stack.Labels = append(stack.Labels, &Label{Name: ProcessTypePProfLabel, Value: value[0]})
+ }
+ process = &Process{
+ PID: pid,
+ ParentPID: ppid,
+ Description: description,
+ Type: ptype,
+ }
+
+ // Now add the dead process back to the map and tree so we don't go back through this again.
+ processMap[process.PID] = process
+ added := false
+ if process.ParentPID != "" && !flat {
+ if parent, ok := processMap[process.ParentPID]; ok {
+ parent.Children = append(parent.Children, process)
+ added = true
+ }
+ }
+ if !added {
+ processes = append(processes, process)
+ }
+ }
+ }
+
+ if process == nil {
+ // This means that the sample we're looking has no PID label
+ var ok bool
+ process, ok = processMap[""]
+ if !ok {
+ // this is the first time we've come acrross an unassociated goroutine so create a "process" to hold them
+ process = &Process{
+ Description: "(unassociated)",
+ Type: NoneProcessType,
+ }
+ processMap[process.PID] = process
+ processes = append(processes, process)
+ }
+ }
+
+ // The sample.Location represents a stack trace for this goroutine,
+ // however each Location can represent multiple lines (mostly due to inlining)
+ // so we need to walk the lines too
+ for _, location := range sample.Location {
+ for _, line := range location.Line {
+ entry := &StackEntry{
+ Function: line.Function.Name,
+ File: line.Function.Filename,
+ Line: int(line.Line),
+ }
+ stack.Entry = append(stack.Entry, entry)
+ }
+ }
+
+ // Now we need a short-descriptive name to call the stack trace if when it is folded and
+ // assuming the stack trace has some lines we'll choose the bottom of the stack (i.e. the
+ // initial function that started the stack trace.) The top of the stack is unlikely to
+ // be very helpful as a lot of the time it will be runtime.select or some other call into
+ // a std library.
+ stack.Description = "(unknown)"
+ if len(stack.Entry) > 0 {
+ stack.Description = stack.Entry[len(stack.Entry)-1].Function
+ }
+
+ process.Stacks = append(process.Stacks, stack)
+ }
+
+ // restrict to not show system processes
+ if noSystem {
+ for i := 0; i < len(processes); i++ {
+ process := processes[i]
+ if process.Type != SystemProcessType && process.Type != NoneProcessType {
+ continue
+ }
+ processes[len(processes)-1], processes[i] = processes[i], processes[len(processes)-1]
+ processes = append(processes[:len(processes)-1], process.Children...)
+ i--
+ }
+ }
+
+ // Now finally re-sort the processes. Newest process appears first
+ after := func(processes []*Process) func(i, j int) bool {
+ return func(i, j int) bool {
+ left, right := processes[i], processes[j]
+ return left.Start.After(right.Start)
+ }
+ }
+ sort.Slice(processes, after(processes))
+ if !flat {
+
+ var sortChildren func(process *Process)
+
+ sortChildren = func(process *Process) {
+ sort.Slice(process.Children, after(process.Children))
+ for _, child := range process.Children {
+ sortChildren(child)
+ }
+ }
+ }
+
+ return processes, processCount, goroutineCount, err
+}
diff --git a/modules/process/manager_test.go b/modules/process/manager_test.go
index 152c7a9235..30eabeb37a 100644
--- a/modules/process/manager_test.go
+++ b/modules/process/manager_test.go
@@ -22,7 +22,7 @@ func TestGetManager(t *testing.T) {
}
func TestManager_AddContext(t *testing.T) {
- pm := Manager{processes: make(map[IDType]*Process), next: 1}
+ pm := Manager{processMap: make(map[IDType]*process), next: 1}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -41,7 +41,7 @@ func TestManager_AddContext(t *testing.T) {
}
func TestManager_Cancel(t *testing.T) {
- pm := Manager{processes: make(map[IDType]*Process), next: 1}
+ pm := Manager{processMap: make(map[IDType]*process), next: 1}
ctx, _, finished := pm.AddContext(context.Background(), "foo")
defer finished()
@@ -69,7 +69,7 @@ func TestManager_Cancel(t *testing.T) {
}
func TestManager_Remove(t *testing.T) {
- pm := Manager{processes: make(map[IDType]*Process), next: 1}
+ pm := Manager{processMap: make(map[IDType]*process), next: 1}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -85,7 +85,7 @@ func TestManager_Remove(t *testing.T) {
pm.Remove(GetPID(p2Ctx))
- _, exists := pm.processes[GetPID(p2Ctx)]
+ _, exists := pm.processMap[GetPID(p2Ctx)]
assert.False(t, exists, "PID %d is in the list but shouldn't", GetPID(p2Ctx))
}
diff --git a/modules/process/process.go b/modules/process/process.go
index 662f878d7f..2f7ea18373 100644
--- a/modules/process/process.go
+++ b/modules/process/process.go
@@ -6,61 +6,34 @@ package process
import (
"context"
- "sync"
"time"
)
-// Process represents a working process inheriting from Gitea.
-type Process struct {
+var (
+ SystemProcessType = "system"
+ RequestProcessType = "request"
+ NormalProcessType = "normal"
+ NoneProcessType = "none"
+)
+
+// process represents a working process inheriting from Gitea.
+type process struct {
PID IDType // Process ID, not system one.
ParentPID IDType
Description string
Start time.Time
Cancel context.CancelFunc
-
- lock sync.Mutex
- children []*Process
-}
-
-// Children gets the children of the process
-// Note: this function will behave nicely even if p is nil
-func (p *Process) Children() (children []*Process) {
- if p == nil {
- return
- }
-
- p.lock.Lock()
- defer p.lock.Unlock()
- children = make([]*Process, len(p.children))
- copy(children, p.children)
- return children
+ Type string
}
-// AddChild adds a child process
-// Note: this function will behave nicely even if p is nil
-func (p *Process) AddChild(child *Process) {
- if p == nil {
- return
- }
-
- p.lock.Lock()
- defer p.lock.Unlock()
- p.children = append(p.children, child)
-}
-
-// RemoveChild removes a child process
-// Note: this function will behave nicely even if p is nil
-func (p *Process) RemoveChild(process *Process) {
- if p == nil {
- return
- }
-
- p.lock.Lock()
- defer p.lock.Unlock()
- for i, child := range p.children {
- if child == process {
- p.children = append(p.children[:i], p.children[i+1:]...)
- return
- }
+// ToProcess converts a process to a externally usable Process
+func (p *process) toProcess() *Process {
+ process := &Process{
+ PID: p.PID,
+ ParentPID: p.ParentPID,
+ Description: p.Description,
+ Start: p.Start,
+ Type: p.Type,
}
+ return process
}