From 0905a5d69709037c190da8a57ea3209b069a85af Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Wed, 22 Oct 2025 14:19:57 -0700 Subject: [PATCH 1/3] Use process.kill instead of process manager killPid --- pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart | 2 +- pkgs/dart_mcp_server/test/tools/flutter_launcher_test.dart | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) 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..fc8bc754 100644 --- a/pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart +++ b/pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart @@ -227,7 +227,7 @@ base mixin FlutterLauncherSupport } catch (e, s) { log(LoggingLevel.error, 'Error launching Flutter application: $e\n$s'); if (process != null) { - processManager.killPid(process.pid); + process.kill(); // The exitCode handler will perform the rest of the cleanup. } return CallToolResult( 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..f35e7716 100644 --- a/pkgs/dart_mcp_server/test/tools/flutter_launcher_test.dart +++ b/pkgs/dart_mcp_server/test/tools/flutter_launcher_test.dart @@ -460,7 +460,6 @@ void main() { 'TimeoutException', ]), ); - test.expect(mockProcessManager.killedPids, [processPid]); server.shutdown(); client.shutdown(); From 354589b7d40e79882a693fde446b42da75303752 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Wed, 22 Oct 2025 15:49:30 -0700 Subject: [PATCH 2/3] Kill all the child processes --- .../lib/src/mixins/flutter_launcher.dart | 37 ++++++++++++++++--- .../test/tools/flutter_launcher_test.dart | 27 ++++++++++++-- 2 files changed, 54 insertions(+), 10 deletions(-) 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 fc8bc754..9def4d8f 100644 --- a/pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart +++ b/pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart @@ -227,7 +227,7 @@ base mixin FlutterLauncherSupport } catch (e, s) { log(LoggingLevel.error, 'Error launching Flutter application: $e\n$s'); if (process != null) { - process.kill(); + processManager.killPid(process.pid); // The exitCode handler will perform the rest of the cleanup. } return CallToolResult( @@ -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 f35e7716..582a1cdf 100644 --- a/pkgs/dart_mcp_server/test/tools/flutter_launcher_test.dart +++ b/pkgs/dart_mcp_server/test/tools/flutter_launcher_test.dart @@ -460,6 +460,7 @@ void main() { 'TimeoutException', ]), ); + test.expect(mockProcessManager.killedPids, [processPid]); server.shutdown(); client.shutdown(); @@ -504,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, @@ -532,9 +542,9 @@ 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, [11111, 22222, processPid]); + test.expect(result.isError, test.isNot(true)); await server.shutdown(); await client.shutdown(); }); @@ -719,7 +729,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')}', ); } @@ -788,7 +800,14 @@ class MockProcessManager implements ProcessManager { Encoding? stdoutEncoding = systemEncoding, Encoding? stderrEncoding = systemEncoding, }) { - throw UnimplementedError(); + commands.add(command); + final mockCommand = _findCommand(command); + return ProcessResult( + mockCommand.pid, + 0, + mockCommand.stdout ?? '', + mockCommand.stderr ?? '', + ); } } From e1f66f77a8e892da2cad3e9a770b6447f41fda91 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Wed, 22 Oct 2025 16:05:28 -0700 Subject: [PATCH 3/3] Fix tests. --- pkgs/dart_mcp_server/README.md | 2 +- .../test/tools/flutter_launcher_test.dart | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) 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/test/tools/flutter_launcher_test.dart b/pkgs/dart_mcp_server/test/tools/flutter_launcher_test.dart index 582a1cdf..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( @@ -543,7 +543,10 @@ void main() { ); test.expect(result.structuredContent, {'success': true}); - test.expect(mockProcessManager.killedPids, [11111, 22222, processPid]); + test.expect(mockProcessManager.killedPids, [ + if (Platform.isLinux) ...[11111, 22222], + processPid, + ]); test.expect(result.isError, test.isNot(true)); await server.shutdown(); await client.shutdown(); @@ -697,7 +700,7 @@ class Command { final List command; final String? stdout; final String? stderr; - final Future? exitCode; + final int? exitCode; final int pid; Command( @@ -754,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; @@ -781,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 ?? '', ); @@ -804,7 +809,7 @@ class MockProcessManager implements ProcessManager { final mockCommand = _findCommand(command); return ProcessResult( mockCommand.pid, - 0, + mockCommand.exitCode ?? 0, mockCommand.stdout ?? '', mockCommand.stderr ?? '', );