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.

Projektseite ↗
HTTPShttps://zanvex.de/git/stackyard.git

internal/operations/process.go

Zum Verzeichnis
package 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)
}