mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-09-11 00:22:56 -05:00
fix(picoclaw): cancel runtime lifecycle on mode switch
Cancel PicoClaw start/readiness waits when MCP takes control, clear held manual input state after failed release reports, and keep paste duration below the control-mode wait budget. Also add MCP copy failure translations and avoid reconnect effect churn when the locale changes.
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
package hid
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestReportLengthValidation(t *testing.T) {
|
||||
h := &Hid{}
|
||||
@@ -14,3 +17,12 @@ func TestReportLengthValidation(t *testing.T) {
|
||||
t.Fatal("expected absolute mouse length error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasteDurationLeavesModeSwitchMargin(t *testing.T) {
|
||||
if maxPasteDuration >= 30*time.Second {
|
||||
t.Fatalf("maxPasteDuration = %s, want below 30s mode switch wait budget", maxPasteDuration)
|
||||
}
|
||||
if got := time.Duration(maxPasteContentRunes) * defaultPasteDelay; got > maxPasteDuration {
|
||||
t.Fatalf("max paste content duration = %s, want <= %s", got, maxPasteDuration)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ type PasteReq struct {
|
||||
|
||||
const (
|
||||
defaultPasteDelay = 30 * time.Millisecond
|
||||
maxPasteDuration = 30 * time.Second
|
||||
maxPasteDuration = 25 * time.Second
|
||||
maxPasteContentRunes = int(maxPasteDuration / defaultPasteDelay)
|
||||
)
|
||||
|
||||
@@ -193,7 +193,7 @@ func (s *Service) Paste(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
if time.Duration(typeableRunes)*defaultPasteDelay > maxPasteDuration {
|
||||
rsp.ErrRsp(c, -2, "paste duration exceeds 30s")
|
||||
rsp.ErrRsp(c, -2, "paste duration exceeds 25s")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -429,6 +429,10 @@ func (s *ManualSession) complete(generation uint64, kind ManualReportKind, held
|
||||
case ManualAbsoluteMouse:
|
||||
s.absoluteMouseHeld = held
|
||||
}
|
||||
} else {
|
||||
s.keyboardHeld = false
|
||||
s.relativeMouseHeld = false
|
||||
s.absoluteMouseHeld = false
|
||||
}
|
||||
releaseControl, end, startCooldown := s.finishIfIdleLocked()
|
||||
s.mu.Unlock()
|
||||
|
||||
@@ -162,6 +162,49 @@ func TestFailedHeldManualInputDoesNotRemainHeld(t *testing.T) {
|
||||
release()
|
||||
}
|
||||
|
||||
func TestFailedReleaseReportClearsHeldManualInput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
kind ManualReportKind
|
||||
}{
|
||||
{name: "keyboard", kind: ManualKeyboard},
|
||||
{name: "relative mouse", kind: ManualRelativeMouse},
|
||||
{name: "absolute mouse", kind: ManualAbsoluteMouse},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
now := time.Unix(260, 0)
|
||||
coordinator := newCoordinator(time.Second, func() time.Time { return now })
|
||||
control := controlmode.NewManager(filepath.Join(t.TempDir(), "mode"), controlmode.ModeMCP)
|
||||
manual := NewManualSession(control, coordinator)
|
||||
defer manual.Close()
|
||||
|
||||
down, err := manual.Reserve(context.Background(), tc.kind, true, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
down.Complete(true)
|
||||
|
||||
up, err := manual.Reserve(context.Background(), tc.kind, false, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
up.Complete(false)
|
||||
|
||||
if _, _, err := coordinator.BeginMCP(context.Background(), OperationHID); !errors.Is(err, ErrManualControlActive) {
|
||||
t.Fatalf("cooldown error = %v, want %v", err, ErrManualControlActive)
|
||||
}
|
||||
now = now.Add(2 * time.Second)
|
||||
_, release, err := coordinator.BeginMCP(context.Background(), OperationHID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed release report left held input active: %v", err)
|
||||
}
|
||||
release()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPointerMoveWithoutButtonsDoesNotStartCooldown(t *testing.T) {
|
||||
coordinator := newCoordinator(time.Second, time.Now)
|
||||
control := controlmode.NewManager(filepath.Join(t.TempDir(), "mode"), controlmode.ModeMCP)
|
||||
@@ -327,6 +370,40 @@ func TestBlockedActiveSessionStillAllowsReleaseReport(t *testing.T) {
|
||||
up.Complete(true)
|
||||
}
|
||||
|
||||
func TestBlockedActiveSessionFailedReleaseClearsHeldReport(t *testing.T) {
|
||||
now := time.Unix(325, 0)
|
||||
coordinator := newCoordinator(time.Second, func() time.Time { return now })
|
||||
control := controlmode.NewManager(filepath.Join(t.TempDir(), "mode"), controlmode.ModePicoclaw)
|
||||
manual := NewManualSession(control, coordinator)
|
||||
defer manual.Close()
|
||||
|
||||
down, err := manual.Reserve(context.Background(), ManualKeyboard, true, func(controlmode.Mode) bool { return true })
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
down.Complete(true)
|
||||
|
||||
if _, err := manual.Reserve(context.Background(), ManualKeyboard, true, func(controlmode.Mode) bool { return false }); !errors.Is(err, ErrManualInputBlocked) {
|
||||
t.Fatalf("new held report error = %v, want %v", err, ErrManualInputBlocked)
|
||||
}
|
||||
|
||||
up, err := manual.Reserve(context.Background(), ManualKeyboard, false, func(controlmode.Mode) bool { return false })
|
||||
if err != nil {
|
||||
t.Fatalf("release report was blocked: %v", err)
|
||||
}
|
||||
up.Complete(false)
|
||||
|
||||
if _, _, err := coordinator.BeginMCP(context.Background(), OperationHID); !errors.Is(err, ErrManualControlActive) {
|
||||
t.Fatalf("cooldown error = %v, want %v", err, ErrManualControlActive)
|
||||
}
|
||||
now = now.Add(2 * time.Second)
|
||||
_, release, err := coordinator.BeginMCP(context.Background(), OperationHID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed blocked release report left held input active: %v", err)
|
||||
}
|
||||
release()
|
||||
}
|
||||
|
||||
func TestReadOnlyMCPAllowedDuringManualControl(t *testing.T) {
|
||||
coordinator := newCoordinator(defaultManualCooldown, time.Now)
|
||||
control := controlmode.NewManager(filepath.Join(t.TempDir(), "mode"), controlmode.ModeMCP)
|
||||
|
||||
@@ -104,6 +104,17 @@ func (s *Service) beginControlOperation(parent context.Context) (context.Context
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) beginRuntimeLifecycleOperation(parent context.Context) (context.Context, func()) {
|
||||
if parent == nil {
|
||||
parent = context.Background()
|
||||
}
|
||||
if s == nil || s.operations == nil {
|
||||
ctx, cancel := context.WithCancelCause(parent)
|
||||
return ctx, func() { cancel(context.Canceled) }
|
||||
}
|
||||
return s.operations.begin(parent)
|
||||
}
|
||||
|
||||
func (s *Service) CancelActiveControlOperations() int {
|
||||
if s == nil || s.operations == nil {
|
||||
return 0
|
||||
@@ -115,6 +126,22 @@ func (s *Service) CancelActiveControlOperations() int {
|
||||
return count
|
||||
}
|
||||
|
||||
func runtimeLifecycleOperationError(ctx context.Context) *PicoclawError {
|
||||
if ctx == nil || ctx.Err() == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cause := context.Cause(ctx)
|
||||
switch {
|
||||
case errors.Is(cause, errControlModeSwitch):
|
||||
return newPicoclawError(CodeControlModeConflict, "PicoClaw runtime lifecycle canceled because the control mode is switching")
|
||||
case errors.Is(cause, context.DeadlineExceeded):
|
||||
return newPicoclawError(CodeRuntimeUnavailable, "PicoClaw runtime lifecycle timed out")
|
||||
default:
|
||||
return newPicoclawError(CodeRuntimeUnavailable, "PicoClaw runtime lifecycle was canceled")
|
||||
}
|
||||
}
|
||||
|
||||
func controlOperationError(ctx context.Context) *PicoclawError {
|
||||
if ctx == nil || ctx.Err() == nil {
|
||||
return nil
|
||||
|
||||
@@ -67,6 +67,70 @@ func TestControlModeSwitchCancelsActiveWait(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestControlModeSwitchCancelsRuntimeLifecycle(t *testing.T) {
|
||||
control := controlmode.NewManager(filepath.Join(t.TempDir(), "mode"), controlmode.ModePicoclaw)
|
||||
service := &Service{
|
||||
control: control,
|
||||
operations: newControlOperationTracker(),
|
||||
}
|
||||
|
||||
operationCtx, releaseOperation := service.beginRuntimeLifecycleOperation(context.Background())
|
||||
releaseMode, modeErr := service.acquireControlMode()
|
||||
if modeErr != nil {
|
||||
t.Fatal(modeErr)
|
||||
}
|
||||
|
||||
lifecycleDone := make(chan *PicoclawError, 1)
|
||||
go func() {
|
||||
<-operationCtx.Done()
|
||||
lifecycleErr := runtimeLifecycleOperationError(operationCtx)
|
||||
releaseOperation()
|
||||
releaseMode()
|
||||
lifecycleDone <- lifecycleErr
|
||||
}()
|
||||
|
||||
switchDone := make(chan error, 1)
|
||||
go func() {
|
||||
switchDone <- control.Switch(controlmode.ModeMCP, func() error {
|
||||
service.CancelActiveControlOperations()
|
||||
return nil
|
||||
})
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-switchDone:
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("control mode switch did not cancel the runtime lifecycle operation")
|
||||
}
|
||||
|
||||
select {
|
||||
case lifecycleErr := <-lifecycleDone:
|
||||
if lifecycleErr == nil || lifecycleErr.Code != CodeControlModeConflict {
|
||||
t.Fatalf("lifecycle error = %+v, want %s", lifecycleErr, CodeControlModeConflict)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("canceled runtime lifecycle operation did not return")
|
||||
}
|
||||
|
||||
if got := control.Current(); got != controlmode.ModeMCP {
|
||||
t.Fatalf("mode = %q, want %q", got, controlmode.ModeMCP)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeReadyWaitHonorsLifecycleCancellation(t *testing.T) {
|
||||
service := &Service{}
|
||||
ctx, cancel := context.WithCancelCause(context.Background())
|
||||
cancel(errControlModeSwitch)
|
||||
|
||||
err := service.waitForRuntimeReadyContext(ctx, time.Hour)
|
||||
if err == nil || err.Code != CodeControlModeConflict {
|
||||
t.Fatalf("error = %+v, want %s", err, CodeControlModeConflict)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitDurationIsBounded(t *testing.T) {
|
||||
service := &Service{}
|
||||
_, err := service.executeAction(context.Background(), Action{
|
||||
|
||||
@@ -35,16 +35,24 @@ func (s *Service) StartRuntime(c *gin.Context) {
|
||||
}
|
||||
defer releaseControl()
|
||||
|
||||
operationCtx, releaseOperation := s.beginRuntimeLifecycleOperation(c.Request.Context())
|
||||
defer releaseOperation()
|
||||
|
||||
unlockLifecycle := s.lockRuntimeLifecycle()
|
||||
defer unlockLifecycle()
|
||||
|
||||
if lifecycleErr := runtimeLifecycleOperationError(operationCtx); lifecycleErr != nil {
|
||||
writePicoclawErrorWithData(c, lifecycleErr, gin.H{"status": s.runtimeStatus()})
|
||||
return
|
||||
}
|
||||
|
||||
if currentStatus := s.runtime.Get(); currentStatus.Installing {
|
||||
runtimeErr := newPicoclawError(CodeRuntimeUnavailable, "picoclaw installation is in progress")
|
||||
writePicoclawErrorWithData(c, runtimeErr, gin.H{"status": s.runtimeStatus()})
|
||||
return
|
||||
}
|
||||
|
||||
if readyErr := s.ensureRuntimeReadyForLifecycle(); readyErr == nil {
|
||||
if readyErr := s.ensureRuntimeReadyForLifecycleContext(operationCtx); readyErr == nil {
|
||||
s.setRuntimeIntentDesired(true, "web")
|
||||
status := s.runtimeStatus()
|
||||
log.WithFields(log.Fields{
|
||||
@@ -67,8 +75,9 @@ func (s *Service) StartRuntime(c *gin.Context) {
|
||||
status.CheckedAt = time.Now()
|
||||
})
|
||||
|
||||
command, output, startErr := s.startRuntime()
|
||||
command, output, startErr := s.startRuntimeContext(operationCtx)
|
||||
if startErr != nil {
|
||||
s.markRuntimeLifecycleCanceled(startErr)
|
||||
s.setRuntimeIntentError(startErr.Message)
|
||||
status := s.runtimeStatus()
|
||||
log.WithFields(log.Fields{
|
||||
|
||||
@@ -14,9 +14,19 @@ import (
|
||||
)
|
||||
|
||||
func (s *Service) startRuntime() (string, string, *PicoclawError) {
|
||||
return s.startRuntimeContext(context.Background())
|
||||
}
|
||||
|
||||
func (s *Service) startRuntimeContext(ctx context.Context) (string, string, *PicoclawError) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if s == nil {
|
||||
return "", "", newPicoclawError(CodeRuntimeStartFailed, "picoclaw service is unavailable")
|
||||
}
|
||||
if lifecycleErr := runtimeLifecycleOperationError(ctx); lifecycleErr != nil {
|
||||
return "", "", lifecycleErr
|
||||
}
|
||||
s.ensureDependencies()
|
||||
|
||||
if installed, statErr := isPicoclawInstalled(); statErr != nil {
|
||||
@@ -77,12 +87,15 @@ func (s *Service) startRuntime() (string, string, *PicoclawError) {
|
||||
}
|
||||
|
||||
command := scriptPath + " start"
|
||||
ctx, cancel := context.WithTimeout(context.Background(), picoclawStartTimeout)
|
||||
startCtx, cancel := context.WithTimeout(ctx, picoclawStartTimeout)
|
||||
defer cancel()
|
||||
|
||||
output, execErr := exec.CommandContext(ctx, "sh", "-c", command).CombinedOutput()
|
||||
output, execErr := exec.CommandContext(startCtx, "sh", "-c", command).CombinedOutput()
|
||||
trimmedOutput := strings.TrimSpace(string(output))
|
||||
if execErr != nil {
|
||||
if lifecycleErr := runtimeLifecycleOperationError(ctx); lifecycleErr != nil {
|
||||
return command, trimmedOutput, lifecycleErr
|
||||
}
|
||||
s.runtime.Update(func(status *RuntimeStatus) {
|
||||
status.Ready = false
|
||||
status.Installed = true
|
||||
@@ -96,17 +109,24 @@ func (s *Service) startRuntime() (string, string, *PicoclawError) {
|
||||
return command, trimmedOutput, newPicoclawError(CodeRuntimeStartFailed, "failed to start picoclaw runtime")
|
||||
}
|
||||
|
||||
time.Sleep(picoclawStartWaitPeriod)
|
||||
if runtimeErr := s.waitForRuntimeReady(picoclawStartTimeout); runtimeErr != nil {
|
||||
if lifecycleErr := sleepRuntimeLifecycleContext(ctx, picoclawStartWaitPeriod); lifecycleErr != nil {
|
||||
return command, trimmedOutput, lifecycleErr
|
||||
}
|
||||
if runtimeErr := s.waitForRuntimeReadyContext(ctx, picoclawStartTimeout); runtimeErr != nil {
|
||||
startErr := newPicoclawError(CodeRuntimeStartFailed, runtimeErr.Message)
|
||||
failureStatus := "unavailable"
|
||||
if cleanupErr := s.stopRuntimeAndVerify(true); cleanupErr != nil {
|
||||
failureStatus = "error"
|
||||
startErr.Message = fmt.Sprintf(
|
||||
"%s; failed to stop partially started runtime: %v",
|
||||
startErr.Message,
|
||||
cleanupErr,
|
||||
)
|
||||
if runtimeErr.Code == CodeControlModeConflict {
|
||||
startErr = runtimeErr
|
||||
failureStatus = "starting"
|
||||
} else {
|
||||
if cleanupErr := s.stopRuntimeAndVerify(true); cleanupErr != nil {
|
||||
failureStatus = "error"
|
||||
startErr.Message = fmt.Sprintf(
|
||||
"%s; failed to stop partially started runtime: %v",
|
||||
startErr.Message,
|
||||
cleanupErr,
|
||||
)
|
||||
}
|
||||
}
|
||||
s.runtime.Update(func(status *RuntimeStatus) {
|
||||
status.Ready = false
|
||||
@@ -121,6 +141,23 @@ func (s *Service) startRuntime() (string, string, *PicoclawError) {
|
||||
return command, trimmedOutput, nil
|
||||
}
|
||||
|
||||
func (s *Service) markRuntimeLifecycleCanceled(err *PicoclawError) {
|
||||
if s == nil || err == nil || err.Code != CodeControlModeConflict {
|
||||
return
|
||||
}
|
||||
s.ensureDependencies()
|
||||
s.runtime.Update(func(status *RuntimeStatus) {
|
||||
status.Ready = false
|
||||
status.Restoring = false
|
||||
if isRuntimeLifecycleStatusPending(*status) {
|
||||
status.Status = "stopped"
|
||||
}
|
||||
status.LastError = err.Message
|
||||
status.CurrentSession = ""
|
||||
status.CheckedAt = time.Now()
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) stopRuntime() (string, string, *PicoclawError) {
|
||||
if s == nil {
|
||||
return "", "", newPicoclawError(CodeRuntimeStartFailed, "picoclaw service is unavailable")
|
||||
@@ -183,11 +220,22 @@ func (s *Service) stopRuntime() (string, string, *PicoclawError) {
|
||||
}
|
||||
|
||||
func (s *Service) waitForRuntimeReady(timeout time.Duration) *PicoclawError {
|
||||
return s.waitForRuntimeReadyContext(context.Background(), timeout)
|
||||
}
|
||||
|
||||
func (s *Service) waitForRuntimeReadyContext(ctx context.Context, timeout time.Duration) *PicoclawError {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
deadline := time.Now().Add(timeout)
|
||||
var lastErr *PicoclawError
|
||||
|
||||
for {
|
||||
runtimeErr := s.ensureRuntimeReadyForLifecycle()
|
||||
if lifecycleErr := runtimeLifecycleOperationError(ctx); lifecycleErr != nil {
|
||||
return lifecycleErr
|
||||
}
|
||||
|
||||
runtimeErr := s.ensureRuntimeReadyForLifecycleContext(ctx)
|
||||
if runtimeErr == nil {
|
||||
return nil
|
||||
} else {
|
||||
@@ -198,7 +246,9 @@ func (s *Service) waitForRuntimeReady(timeout time.Duration) *PicoclawError {
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(picoclawReadyPollPeriod)
|
||||
if lifecycleErr := sleepRuntimeLifecycleContext(ctx, picoclawReadyPollPeriod); lifecycleErr != nil {
|
||||
return lifecycleErr
|
||||
}
|
||||
}
|
||||
|
||||
if lastErr != nil {
|
||||
@@ -218,18 +268,31 @@ func resolvePicoclawStartScript() (string, error) {
|
||||
}
|
||||
|
||||
func runPicoclawOnboard() (string, *PicoclawError) {
|
||||
return runPicoclawOnboardContext(context.Background())
|
||||
}
|
||||
|
||||
func runPicoclawOnboardContext(ctx context.Context) (string, *PicoclawError) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
scriptPath, err := resolvePicoclawStartScript()
|
||||
if err != nil {
|
||||
return "", newPicoclawError(CodeRuntimeUnavailable, err.Error())
|
||||
}
|
||||
if lifecycleErr := runtimeLifecycleOperationError(ctx); lifecycleErr != nil {
|
||||
return "", lifecycleErr
|
||||
}
|
||||
|
||||
command := scriptPath + " onboard"
|
||||
ctx, cancel := context.WithTimeout(context.Background(), picoclawOnboardTimeout)
|
||||
onboardCtx, cancel := context.WithTimeout(ctx, picoclawOnboardTimeout)
|
||||
defer cancel()
|
||||
|
||||
output, execErr := exec.CommandContext(ctx, "sh", "-c", command).CombinedOutput()
|
||||
output, execErr := exec.CommandContext(onboardCtx, "sh", "-c", command).CombinedOutput()
|
||||
trimmedOutput := strings.TrimSpace(string(output))
|
||||
if execErr != nil {
|
||||
if lifecycleErr := runtimeLifecycleOperationError(ctx); lifecycleErr != nil {
|
||||
return trimmedOutput, lifecycleErr
|
||||
}
|
||||
if trimmedOutput == "" {
|
||||
trimmedOutput = execErr.Error()
|
||||
}
|
||||
@@ -239,6 +302,23 @@ func runPicoclawOnboard() (string, *PicoclawError) {
|
||||
return trimmedOutput, nil
|
||||
}
|
||||
|
||||
func sleepRuntimeLifecycleContext(ctx context.Context, delay time.Duration) *PicoclawError {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if delay <= 0 {
|
||||
return runtimeLifecycleOperationError(ctx)
|
||||
}
|
||||
timer := time.NewTimer(delay)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return runtimeLifecycleOperationError(ctx)
|
||||
case <-timer.C:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func isPicoclawInstalled() (bool, error) {
|
||||
info, err := os.Stat(picoclawBinaryPath)
|
||||
if err == nil {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package picoclaw
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -355,17 +356,24 @@ func (s *Service) lockRuntimeLifecycle() func() {
|
||||
}
|
||||
|
||||
func (s *Service) ensureRuntimeReady() *PicoclawError {
|
||||
return s.ensureRuntimeReadyWithProbeProtection(false)
|
||||
return s.ensureRuntimeReadyWithProbeProtection(context.Background(), false)
|
||||
}
|
||||
|
||||
func (s *Service) ensureRuntimeReadyForLifecycle() *PicoclawError {
|
||||
return s.ensureRuntimeReadyWithProbeProtection(true)
|
||||
return s.ensureRuntimeReadyForLifecycleContext(context.Background())
|
||||
}
|
||||
|
||||
func (s *Service) ensureRuntimeReadyWithProbeProtection(allowLifecycleOverwrite bool) *PicoclawError {
|
||||
func (s *Service) ensureRuntimeReadyForLifecycleContext(ctx context.Context) *PicoclawError {
|
||||
return s.ensureRuntimeReadyWithProbeProtection(ctx, true)
|
||||
}
|
||||
|
||||
func (s *Service) ensureRuntimeReadyWithProbeProtection(ctx context.Context, allowLifecycleOverwrite bool) *PicoclawError {
|
||||
if s == nil {
|
||||
return newPicoclawError(CodeRuntimeUnavailable, "picoclaw service is unavailable")
|
||||
}
|
||||
if lifecycleErr := runtimeLifecycleOperationError(ctx); lifecycleErr != nil {
|
||||
return lifecycleErr
|
||||
}
|
||||
s.ensureDependencies()
|
||||
setStatus := func(status RuntimeStatus) {
|
||||
if allowLifecycleOverwrite {
|
||||
@@ -440,7 +448,10 @@ func (s *Service) ensureRuntimeReadyWithProbeProtection(allowLifecycleOverwrite
|
||||
}
|
||||
if _, err := os.Stat(configPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if _, onboardErr := runPicoclawOnboard(); onboardErr != nil {
|
||||
if lifecycleErr := runtimeLifecycleOperationError(ctx); lifecycleErr != nil {
|
||||
return lifecycleErr
|
||||
}
|
||||
if _, onboardErr := runPicoclawOnboardContext(ctx); onboardErr != nil {
|
||||
setStatus(RuntimeStatus{
|
||||
Ready: false,
|
||||
Installed: true,
|
||||
@@ -455,6 +466,9 @@ func (s *Service) ensureRuntimeReadyWithProbeProtection(allowLifecycleOverwrite
|
||||
})
|
||||
return newPicoclawError(CodeRuntimeUnavailable, "picoclaw model is not configured")
|
||||
}
|
||||
if lifecycleErr := runtimeLifecycleOperationError(ctx); lifecycleErr != nil {
|
||||
return lifecycleErr
|
||||
}
|
||||
if _, statErr := os.Stat(configPath); statErr == nil {
|
||||
goto configReady
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user