Git Repository
Stackyard
Stackyard scannt lokale Entwicklungsordner und bündelt Projekte, Git-Status, TODOs, Toolchains, Docker-Compose-Setups, Datei- und Launcher-Funktionen sowie Operations-Informationen in einer lokalen WebUI.
HTTPS
https://zanvex.de/git/stackyard.gitinternal/operations/process.go
Zum Verzeichnispackage operations
import (
"errors"
"fmt"
"os"
"os/exec"
"sync"
"time"
)
const maxProcessLogSize = 128 << 10
type ProcessStatus struct {
ProjectID string
Kind string
Running bool
PID int
StartedAt time.Time
StoppedAt time.Time
Error string
Log string
}
type managedProcess struct {
status ProcessStatus
cmd *exec.Cmd
log *boundedLog
}
type ProcessManager struct {
mu sync.Mutex
processes map[string]*managedProcess
}
func NewProcessManager() *ProcessManager {
return &ProcessManager{processes: make(map[string]*managedProcess)}
}
func processKey(projectID, kind string) string {
return projectID + "\x00" + kind
}
func (m *ProcessManager) Start(projectID, projectDir, kind string) error {
if m == nil {
return errors.New("process manager is unavailable")
}
args, ok := allowedActions[kind]
if !ok || !IsLongRunningAction(kind) {
return fmt.Errorf("action %q cannot be started as a service", kind)
}
info, err := os.Stat(projectDir)
if err != nil || !info.IsDir() {
return errors.New("project directory is unavailable")
}
key := processKey(projectID, kind)
m.mu.Lock()
if current := m.processes[key]; current != nil && current.status.Running {
m.mu.Unlock()
return errors.New("action is already running")
}
log := &boundedLog{max: maxProcessLogSize}
cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = projectDir
cmd.Stdout = log
cmd.Stderr = log
if err := cmd.Start(); err != nil {
m.mu.Unlock()
return fmt.Errorf("start %s: %w", kind, err)
}
current := &managedProcess{
cmd: cmd,
log: log,
status: ProcessStatus{
ProjectID: projectID,
Kind: kind,
Running: true,
PID: cmd.Process.Pid,
StartedAt: time.Now(),
},
}
m.processes[key] = current
m.mu.Unlock()
go m.wait(key, current)
return nil
}
func (m *ProcessManager) wait(key string, current *managedProcess) {
err := current.cmd.Wait()
m.mu.Lock()
defer m.mu.Unlock()
if m.processes[key] != current {
return
}
current.status.Running = false
current.status.PID = 0
current.status.StoppedAt = time.Now()
if err != nil {
current.status.Error = err.Error()
}
}
func (m *ProcessManager) Stop(projectID, kind string) error {
if m == nil {
return errors.New("process manager is unavailable")
}
key := processKey(projectID, kind)
m.mu.Lock()
current := m.processes[key]
if current == nil || !current.status.Running || current.cmd.Process == nil {
m.mu.Unlock()
return errors.New("action is not running")
}
process := current.cmd.Process
m.mu.Unlock()
if err := terminateProcess(process); err != nil {
return fmt.Errorf("stop %s: %w", kind, err)
}
return nil
}
func (m *ProcessManager) Status(projectID, kind string) ProcessStatus {
if m == nil {
return ProcessStatus{ProjectID: projectID, Kind: kind}
}
m.mu.Lock()
defer m.mu.Unlock()
current := m.processes[processKey(projectID, kind)]
if current == nil {
return ProcessStatus{ProjectID: projectID, Kind: kind}
}
status := current.status
status.Log = current.log.String()
return status
}
func (m *ProcessManager) StopAll() {
if m == nil {
return
}
m.mu.Lock()
processes := make([]*os.Process, 0, len(m.processes))
for _, current := range m.processes {
if current.status.Running && current.cmd.Process != nil {
processes = append(processes, current.cmd.Process)
}
}
m.mu.Unlock()
for _, process := range processes {
_ = terminateProcess(process)
}
}
type boundedLog struct {
mu sync.Mutex
data []byte
max int
}
func (b *boundedLog) Write(p []byte) (int, error) {
b.mu.Lock()
defer b.mu.Unlock()
b.data = append(b.data, p...)
if len(b.data) > b.max {
b.data = append([]byte(nil), b.data[len(b.data)-b.max:]...)
}
return len(p), nil
}
func (b *boundedLog) String() string {
b.mu.Lock()
defer b.mu.Unlock()
return string(b.data)
}