Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

182 lines
4.9 KiB
Go

package workspace
import (
"context"
"log/slog"
"sync/atomic"
"time"
workspacebiz "github.com/tutti-os/tutti/services/tuttid/biz/workspace"
)
var appRuntimeUpdatedUnixMs atomic.Int64
func (r *AppRunner) setRunningIfProcessCurrent(key string, start *appStart, process *appProcess, state workspacebiz.AppRuntimeState) bool {
r.mu.Lock()
current := r.states[key]
if r.starts[key] != start || r.processes[key] != process || process.stopRequested || current.Status != workspacebiz.AppRuntimeStatusStarting {
r.mu.Unlock()
return false
}
updated := withRuntimeUpdated(state)
r.states[key] = updated
r.mu.Unlock()
r.notifyStateChanged(key, updated)
return true
}
func (r *AppRunner) setFailedForStart(key string, start *appStart, reason string, err error) (workspacebiz.AppRuntimeState, bool) {
message := ""
if err != nil {
message = err.Error()
}
failurePhase := workspacebiz.AppFailurePhaseStarting
return r.setStateForStart(key, start, workspacebiz.AppRuntimeState{
Status: workspacebiz.AppRuntimeStatusFailed,
FailurePhase: &failurePhase,
FailureReason: &reason,
LastError: &message,
})
}
func (r *AppRunner) setStateForStart(key string, start *appStart, state workspacebiz.AppRuntimeState) (workspacebiz.AppRuntimeState, bool) {
r.mu.Lock()
if r.starts[key] != start {
current := currentRuntimeStateOrIdle(r.states[key])
r.mu.Unlock()
return current, false
}
updated := withRuntimeUpdated(state)
r.states[key] = updated
r.mu.Unlock()
r.notifyStateChanged(key, updated)
return updated, true
}
func (r *AppRunner) setState(key string, state workspacebiz.AppRuntimeState) workspacebiz.AppRuntimeState {
r.ensure()
r.mu.Lock()
updated := withRuntimeUpdated(state)
r.states[key] = updated
r.mu.Unlock()
r.notifyStateChanged(key, updated)
return updated
}
func (r *AppRunner) notifyStateChanged(key string, state workspacebiz.AppRuntimeState) {
if r.OnStateChanged == nil {
return
}
r.OnStateChanged(appRuntimeWorkspaceIDFromKey(key), appRuntimeAppIDFromKey(key), state)
}
func (r *AppRunner) waitForProcess(key string, process *appProcess) {
err := process.command.Wait()
if process.containment != nil {
_ = process.containment.close()
}
_ = process.logFile.Close()
r.mu.Lock()
if current := r.processes[key]; current != process {
r.mu.Unlock()
process.done <- err
return
}
delete(r.processes, key)
currentState := r.states[key]
var nextState workspacebiz.AppRuntimeState
if process.stopRequested {
nextState = withRuntimeUpdated(workspacebiz.AppRuntimeState{Status: workspacebiz.AppRuntimeStatusIdle})
} else {
message := "workspace app process exited unexpectedly with status 0"
if err != nil {
message = err.Error()
}
failurePhase := workspacebiz.AppFailurePhaseStarting
if currentState.Status == workspacebiz.AppRuntimeStatusRunning {
failurePhase = workspacebiz.AppFailurePhaseRuntime
}
nextState = withRuntimeUpdated(workspacebiz.AppRuntimeState{
Status: workspacebiz.AppRuntimeStatusFailed,
FailurePhase: &failurePhase,
FailureReason: stringPtr("process_exit"),
LastError: &message,
})
}
if !process.stopRequested {
lastError := ""
if nextState.LastError != nil {
lastError = *nextState.LastError
}
slog.Warn(
"workspace_app_runtime_process_failed",
"workspaceId", appRuntimeWorkspaceIDFromKey(key),
"appId", appRuntimeAppIDFromKey(key),
"failureReason", "process_exit",
"lastError", lastError,
"error", err,
)
}
r.states[key] = nextState
r.mu.Unlock()
r.notifyStateChanged(key, nextState)
process.done <- err
}
func (r *AppRunner) finishStart(key string, ctx context.Context, start *appStart) {
r.mu.Lock()
defer r.mu.Unlock()
currentStart := r.starts[key]
if currentStart == nil || currentStart != start {
return
}
delete(r.starts, key)
var nextState *workspacebiz.AppRuntimeState
if ctx.Err() != nil && r.processes[key] == nil {
current := r.states[key]
if current.Status == workspacebiz.AppRuntimeStatusPreparing || current.Status == workspacebiz.AppRuntimeStatusStarting {
state := withRuntimeUpdated(workspacebiz.AppRuntimeState{
Status: workspacebiz.AppRuntimeStatusIdle,
})
r.states[key] = state
nextState = &state
}
}
if nextState != nil {
go r.notifyStateChanged(key, *nextState)
}
}
func waitForDetachedAppProcess(process *appProcess) {
err := process.command.Wait()
if process.containment != nil {
_ = process.containment.close()
}
_ = process.logFile.Close()
process.done <- err
}
func withRuntimeUpdated(state workspacebiz.AppRuntimeState) workspacebiz.AppRuntimeState {
now := unixMsNow()
for {
previous := appRuntimeUpdatedUnixMs.Load()
if now <= previous {
now = previous + 1
}
if appRuntimeUpdatedUnixMs.CompareAndSwap(previous, now) {
break
}
}
state.UpdatedAtUnixMs = &now
return state
}
func unixMsNow() int64 {
return time.Now().UTC().UnixNano() / int64(time.Millisecond)
}
func stringPtr(value string) *string {
return &value
}