diff --git a/pkgs/dart_mcp_server/README.md b/pkgs/dart_mcp_server/README.md index 8431c3e0..48223891 100644 --- a/pkgs/dart_mcp_server/README.md +++ b/pkgs/dart_mcp_server/README.md @@ -160,6 +160,6 @@ For more information, see the official VS Code documentation for | `run_tests` | Run tests | Run Dart or Flutter tests with an agent centric UX. ALWAYS use instead of `dart test` or `flutter test` shell commands. | | `set_widget_selection_mode` | Set Widget Selection Mode | Enables or disables widget selection mode in the active Flutter application. Requires "connect_dart_tooling_daemon" to be successfully called first. This is not necessary when using flutter driver, only use it when you want the user to select a widget. | | `signature_help` | Signature help | Get signature help for an API being used at a given cursor position in a file. | -| `stop_app` | | Kills a running Flutter process started by the launch_app tool. | +| `stop_app` | | Stops a running Flutter process started by the launch_app tool. | diff --git a/pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart b/pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart index 00cf5e2e..9def4d8f 100644 --- a/pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart +++ b/pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart @@ -243,11 +243,11 @@ base mixin FlutterLauncherSupport final stopAppTool = Tool( name: 'stop_app', description: - 'Kills a running Flutter process started by the launch_app tool.', + 'Stops a running Flutter process started by the launch_app tool.', inputSchema: Schema.object( properties: { 'pid': Schema.int( - description: 'The process ID of the process to kill.', + description: 'The process ID of the process to stop.', ), }, required: ['pid'], @@ -255,7 +255,7 @@ base mixin FlutterLauncherSupport outputSchema: Schema.object( properties: { 'success': Schema.bool( - description: 'Whether the process was killed successfully.', + description: 'Whether the process was stopped successfully.', ), }, required: ['success'], @@ -274,8 +274,33 @@ base mixin FlutterLauncherSupport content: [TextContent(text: 'Application with PID $pid not found.')], ); } - - final success = processManager.killPid(pid); + // On Unix, killing the flutter process doesn't kill the entire process + // group, so we have to look for the child processes. + if (Platform.isLinux) { + final ps = processManager.runSync([ + 'ps', + '--no-headers', + '--format', + '%p', + '--ppid', + '$pid', + ]); + if (ps.exitCode == 0) { + final children = (ps.stdout as String).trim().split('\n'); + if (children.isNotEmpty) { + for (final child in children) { + int childPid; + try { + childPid = int.parse(child); + } on FormatException { + continue; + } + processManager.killPid(childPid, ProcessSignal.sigterm); + } + } + } + } + final success = processManager.killPid(app.process.pid); if (success) { log( LoggingLevel.info, diff --git a/pkgs/dart_mcp_server/test/tools/flutter_launcher_test.dart b/pkgs/dart_mcp_server/test/tools/flutter_launcher_test.dart index 09ae3656..7c25f0a5 100644 --- a/pkgs/dart_mcp_server/test/tools/flutter_launcher_test.dart +++ b/pkgs/dart_mcp_server/test/tools/flutter_launcher_test.dart @@ -318,7 +318,7 @@ void main() { 'test-device', ], stderr: 'Something went wrong', - exitCode: Future.value(1), + exitCode: 1, ), ); final serverAndClient = await createServerAndClient( @@ -505,6 +505,15 @@ void main() { pid: processPid, ), ); + if (Platform.isLinux) { + mockProcessManager.addCommand( + Command( + ['ps', '--no-headers', '--format', '%p', '--ppid', '$processPid'], + stdout: '11111\n22222\n', + pid: processPid, + ), + ); + } final serverAndClient = await createServerAndClient( processManager: mockProcessManager, fileSystem: fileSystem, @@ -533,9 +542,12 @@ void main() { CallToolRequest(name: 'stop_app', arguments: {'pid': processPid}), ); - test.expect(result.isError, test.isNot(true)); test.expect(result.structuredContent, {'success': true}); - test.expect(mockProcessManager.killedPids, [processPid]); + test.expect(mockProcessManager.killedPids, [ + if (Platform.isLinux) ...[11111, 22222], + processPid, + ]); + test.expect(result.isError, test.isNot(true)); await server.shutdown(); await client.shutdown(); }); @@ -688,7 +700,7 @@ class Command { final List command; final String? stdout; final String? stderr; - final Future? exitCode; + final int? exitCode; final int pid; Command( @@ -720,7 +732,9 @@ class MockProcessManager implements ProcessManager { } } throw Exception( - 'Command not mocked: $command. Mocked commands:\n${_commands.join('\n')}', + 'Command not mocked: "${command.join(' ')}".\n' + 'Mocked commands:\n' + '${_commands.map((e) => e.command.join(' ')).join('\n')}', ); } @@ -743,7 +757,9 @@ class MockProcessManager implements ProcessManager { stdout: Stream.value(utf8.encode(mockCommand.stdout ?? '')), stderr: Stream.value(utf8.encode(mockCommand.stderr ?? '')), pid: pid, - exitCodeFuture: mockCommand.exitCode, + exitCodeFuture: mockCommand.exitCode != null + ? Future(() => mockCommand.exitCode!) + : null, ); runningProcesses[pid] = process; return process; @@ -770,7 +786,7 @@ class MockProcessManager implements ProcessManager { final mockCommand = _findCommand(command); return ProcessResult( mockCommand.pid, - await (mockCommand.exitCode ?? Future.value(0)), + mockCommand.exitCode ?? 0, mockCommand.stdout ?? '', mockCommand.stderr ?? '', ); @@ -789,7 +805,14 @@ class MockProcessManager implements ProcessManager { Encoding? stdoutEncoding = systemEncoding, Encoding? stderrEncoding = systemEncoding, }) { - throw UnimplementedError(); + commands.add(command); + final mockCommand = _findCommand(command); + return ProcessResult( + mockCommand.pid, + mockCommand.exitCode ?? 0, + mockCommand.stdout ?? '', + mockCommand.stderr ?? '', + ); } }