From c8e73e780987856fe9e61f6c1e6c9e7a5c56de0a Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Wed, 29 Jul 2026 10:35:36 +0300 Subject: [PATCH] fix(sandbox): prevent false-positive SIGKILL in SandboxWatchdog during heavy RPC --- .../extensions/rpc/json_rpc_stdio_client.dart | 6 ++++ .../extensions/rpc/plugin_rpc_bridge.dart | 1 + .../extensions/sandbox/sandbox_watchdog.dart | 8 ++++-- .../sandbox/sandbox_watchdog_test.dart | 28 +++++++++++++++++++ 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/lib/core/extensions/rpc/json_rpc_stdio_client.dart b/lib/core/extensions/rpc/json_rpc_stdio_client.dart index 130f56f5..4e0e5445 100644 --- a/lib/core/extensions/rpc/json_rpc_stdio_client.dart +++ b/lib/core/extensions/rpc/json_rpc_stdio_client.dart @@ -41,6 +41,12 @@ class JsonRpcStdioClient { StreamSubscription? _subscription; Object? _fatalError; + /// Number of RPC requests currently in-flight waiting for a response. + int get pendingRequestCount => _pending.length; + + /// Returns true when there is at least one active RPC request in-flight. + bool get hasPendingRequests => _pending.isNotEmpty; + /// Serializes async line handling so large-line isolate decode stays ordered. Future _lineChain = Future.value(); diff --git a/lib/core/extensions/rpc/plugin_rpc_bridge.dart b/lib/core/extensions/rpc/plugin_rpc_bridge.dart index 9e528bb3..4b0a61b9 100644 --- a/lib/core/extensions/rpc/plugin_rpc_bridge.dart +++ b/lib/core/extensions/rpc/plugin_rpc_bridge.dart @@ -117,6 +117,7 @@ class PluginRpcBridge { if (enableWatchdog) { _watchdog = SandboxWatchdog( recovery: _recovery, + isBusy: () => client.hasPendingRequests, onStopped: (reason) { if (reason == SandboxWatchdogStopReason.deadlock) { unawaited(_audit?.record( diff --git a/lib/core/extensions/sandbox/sandbox_watchdog.dart b/lib/core/extensions/sandbox/sandbox_watchdog.dart index 4a8c096a..16a25e10 100644 --- a/lib/core/extensions/sandbox/sandbox_watchdog.dart +++ b/lib/core/extensions/sandbox/sandbox_watchdog.dart @@ -25,16 +25,19 @@ enum SandboxWatchdogStopReason { class SandboxWatchdog { SandboxWatchdog({ this.pingInterval = const Duration(seconds: 30), - this.pongTimeout = const Duration(seconds: 5), + this.pongTimeout = const Duration(seconds: 15), this.recovery, + bool Function()? isBusy, Future Function()? ping, void Function(SandboxWatchdogStopReason reason)? onStopped, - }) : _pingOverride = ping, + }) : _isBusy = isBusy, + _pingOverride = ping, _onStopped = onStopped; final Duration pingInterval; final Duration pongTimeout; final SandboxAutoRecovery? recovery; + final bool Function()? _isBusy; final Future Function()? _pingOverride; final void Function(SandboxWatchdogStopReason reason)? _onStopped; @@ -107,6 +110,7 @@ class SandboxWatchdog { Future _tick() async { if (!_running || _pingInFlight) return; + if (_isBusy?.call() ?? false) return; _pingInFlight = true; try { final result = await _sendPing().timeout(pongTimeout); diff --git a/test/core/extensions/sandbox/sandbox_watchdog_test.dart b/test/core/extensions/sandbox/sandbox_watchdog_test.dart index d6038503..cbf08f2c 100644 --- a/test/core/extensions/sandbox/sandbox_watchdog_test.dart +++ b/test/core/extensions/sandbox/sandbox_watchdog_test.dart @@ -254,6 +254,34 @@ void main() { await client.close(); await handle.dispose(); }); + + test('skips ping tick when isBusy returns true', () async { + final process = _FakeProcess(); + final handle = await _handle(process, tempBase); + var pings = 0; + var busy = true; + + final watchdog = SandboxWatchdog( + pingInterval: const Duration(milliseconds: 15), + pongTimeout: const Duration(seconds: 1), + isBusy: () => busy, + ping: () async { + pings++; + return 'pong'; + }, + ); + + watchdog.start(handle); + await Future.delayed(const Duration(milliseconds: 50)); + expect(pings, 0, reason: 'Pings should be skipped while busy'); + + busy = false; + await Future.delayed(const Duration(milliseconds: 50)); + expect(pings, greaterThan(0), reason: 'Pings should resume when idle'); + + watchdog.stop(); + await handle.dispose(); + }); }); group('SandboxWatchdog.isPong', () {