120 lines
2.9 KiB
Go
120 lines
2.9 KiB
Go
package process
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"sync"
|
|
|
|
"codespace/internal/util"
|
|
)
|
|
|
|
// Manager manages OpenCode processes per workspace.
|
|
type Manager interface {
|
|
Start(workspaceID string, workspaceRoot string) error
|
|
Stop(workspaceID string) error
|
|
Restart(workspaceID string, workspaceRoot string) error
|
|
Status(workspaceID string) Status
|
|
}
|
|
|
|
// LocalManager implements Manager using local OS processes.
|
|
type LocalManager struct {
|
|
command string
|
|
args []string
|
|
mu sync.Mutex
|
|
sessions map[string]*Session
|
|
}
|
|
|
|
// NewManager creates a LocalManager with the given command.
|
|
// If command is empty, it defaults to "opencode".
|
|
func NewManager(command string) *LocalManager {
|
|
return &LocalManager{
|
|
command: normalizeCommand(command),
|
|
args: []string{},
|
|
sessions: make(map[string]*Session),
|
|
}
|
|
}
|
|
|
|
// Start launches a process for the given workspace.
|
|
// Returns CodeConflict if a session is already running.
|
|
func (m *LocalManager) Start(workspaceID string, workspaceRoot string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
if sess, ok := m.sessions[workspaceID]; ok && sess.Cmd.Process != nil {
|
|
if sess.Cmd.ProcessState == nil {
|
|
return util.New(util.CodeConflict, "process already running")
|
|
}
|
|
}
|
|
|
|
cmd := exec.Command(m.command, m.args...)
|
|
cmd.Dir = workspaceRoot
|
|
cmd.Env = append(os.Environ(), fmt.Sprintf("HOME=%s", workspaceRoot))
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
return util.Wrap(util.CodeInternal, "failed to start process", err)
|
|
}
|
|
|
|
m.sessions[workspaceID] = &Session{
|
|
WorkspaceID: workspaceID,
|
|
Root: workspaceRoot,
|
|
Cmd: cmd,
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop kills the process for the given workspace.
|
|
// Returns CodeNotFound if no session exists.
|
|
func (m *LocalManager) Stop(workspaceID string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
sess, ok := m.sessions[workspaceID]
|
|
if !ok || sess.Cmd.Process == nil {
|
|
return util.New(util.CodeNotFound, "no running process for workspace")
|
|
}
|
|
|
|
if err := sess.Cmd.Process.Kill(); err != nil {
|
|
return util.Wrap(util.CodeInternal, "failed to stop process", err)
|
|
}
|
|
|
|
delete(m.sessions, workspaceID)
|
|
return nil
|
|
}
|
|
|
|
// Restart stops (if running) then starts the process.
|
|
func (m *LocalManager) Restart(workspaceID string, workspaceRoot string) error {
|
|
m.mu.Lock()
|
|
if sess, ok := m.sessions[workspaceID]; ok && sess.Cmd.Process != nil {
|
|
if sess.Cmd.ProcessState == nil {
|
|
_ = sess.Cmd.Process.Kill()
|
|
delete(m.sessions, workspaceID)
|
|
}
|
|
}
|
|
m.mu.Unlock()
|
|
|
|
return m.Start(workspaceID, workspaceRoot)
|
|
}
|
|
|
|
// Status returns the current process status for the workspace.
|
|
func (m *LocalManager) Status(workspaceID string) Status {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
sess, ok := m.sessions[workspaceID]
|
|
if !ok || sess.Cmd.Process == nil {
|
|
return Status{WorkspaceID: workspaceID, Running: false}
|
|
}
|
|
|
|
if sess.Cmd.ProcessState != nil {
|
|
return Status{WorkspaceID: workspaceID, Running: false}
|
|
}
|
|
|
|
return Status{
|
|
WorkspaceID: workspaceID,
|
|
Running: true,
|
|
PID: sess.Cmd.Process.Pid,
|
|
}
|
|
}
|