Skip to content

Commit 76d8e58

Browse files
authored
Add reflection utility to fix split debugger error (#8878)
Fixes #8831 There is an API change somewhere in 2025.3 which causes the error to be thrown during debugging. I considered making this a separate plugin branch in #8877. To test this change in IntelliJ IDEA 2026.1: - Debug an app - Debug a test - Attach an app (Run menu > Flutter Attach, then start the app on device) - I don't have a way to test bazel code. It should probably be removed.
1 parent dd0088d commit 76d8e58

6 files changed

Lines changed: 103 additions & 23 deletions

File tree

.gemini/styleguide.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ enforce standard modern Java/Kotlin coding conventions, but strictly police the
1313
- `[NIT]`: Idiomatic improvements or minor naming suggestions.
1414
- **Focus:** Prioritize logic, performance on the UI thread, and architectural consistency.
1515
- **No Empty Praise:** Do not leave "Looks good" or "Nice change" comments. If there are no issues, leave no comments.
16+
- **Copyright Headers:** Ensure all new files have a proper copyright header (e.g., `Copyright 2026 The Chromium Authors`). Flag any missing
17+
headers as `[MUST-FIX]`.
1618

1719
## 2. IntelliJ Platform Best Practices
1820

@@ -30,8 +32,10 @@ enforce standard modern Java/Kotlin coding conventions, but strictly police the
3032
as recommended by the modern SDK.
3133
- **Backward Compatibility:** Avoid using `@ApiStatus.Internal` or `@ApiStatus.ScheduledForRemoval` APIs unless strictly necessary.
3234
- **Logging:**
33-
- Reject any use of `System.out.println` or `System.err.println` for logging in `src/` code (integration tests may use them for milestone logging).
34-
- Enforce the use of the IntelliJ SDK's built-in logger (`com.intellij.openapi.diagnostic.Logger`) or our own (`io.flutter.logging.PluginLogger`).
35+
- Reject any use of `System.out.println` or `System.err.println` for logging in `src/` code (integration tests may use them for
36+
milestone logging).
37+
- Enforce the use of the IntelliJ SDK's built-in logger (`com.intellij.openapi.diagnostic.Logger`) or our own (
38+
`io.flutter.logging.PluginLogger`).
3539
- **Actions:**
3640
- Classes extending `AnAction` must be completely stateless. Flag any `AnAction` class that defines mutable instance variables (fields),
3741
as the platform instantiates a single instance of the action for the lifetime of the IDE.

src/io/flutter/run/AttachState.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,6 @@ protected RunContentDescriptor launch(@NotNull ExecutionEnvironment env) throws
4747
// Cache for use in console configuration, and for updating registered extensionRPCs.
4848
FlutterApp.addToEnvironment(env, app);
4949
final ExecutionResult result = setUpConsoleAndActions(app);
50-
return createDebugSession(env, app, result).getRunContentDescriptor();
50+
return createDebugSession(env, app, result);
5151
}
5252
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright 2026 The Chromium Authors. All rights reserved.
3+
* Use of this source code is governed by a BSD-style license that can be
4+
* found in the LICENSE file.
5+
*/
6+
package io.flutter.run;
7+
8+
import com.intellij.execution.ExecutionException;
9+
import com.intellij.execution.runners.ExecutionEnvironment;
10+
import com.intellij.execution.ui.RunContentDescriptor;
11+
import com.intellij.xdebugger.XDebugProcessStarter;
12+
import com.intellij.xdebugger.XDebugSession;
13+
import com.intellij.xdebugger.XDebuggerManager;
14+
import org.jetbrains.annotations.NotNull;
15+
16+
import java.lang.reflect.InvocationTargetException;
17+
import java.lang.reflect.Method;
18+
19+
// TODO(helin24): This class is using reflection to find experimental APIs that are only present in platform versions 2025.3+. We should be
20+
// able to use the APIs directly once we are only supporting versions past 2025.3.
21+
// See https://github.com/flutter/flutter-intellij/issues/8879.
22+
public class FlutterDebugSessionUtils {
23+
24+
private static final Method newSessionBuilderMethod;
25+
private static final Method environmentMethod;
26+
private static final Method startSessionMethod;
27+
28+
static {
29+
Method nsb = null;
30+
Method env = null;
31+
Method ss = null;
32+
try {
33+
nsb = XDebuggerManager.class.getMethod("newSessionBuilder", XDebugProcessStarter.class);
34+
Class<?> builderClass = nsb.getReturnType();
35+
env = builderClass.getMethod("environment", ExecutionEnvironment.class);
36+
ss = builderClass.getMethod("startSession");
37+
} catch (NoSuchMethodException e) {
38+
// Fallback for older platforms
39+
}
40+
newSessionBuilderMethod = nsb;
41+
environmentMethod = env;
42+
startSessionMethod = ss;
43+
}
44+
45+
public static @NotNull RunContentDescriptor startSessionAndGetDescriptor(
46+
@NotNull XDebuggerManager manager,
47+
@NotNull ExecutionEnvironment env,
48+
@NotNull XDebugProcessStarter starter,
49+
boolean muteBreakpoints) throws ExecutionException {
50+
try {
51+
if (newSessionBuilderMethod == null) {
52+
throw new NoSuchMethodException("newSessionBuilder is not available");
53+
}
54+
Object builder = newSessionBuilderMethod.invoke(manager, starter);
55+
builder = environmentMethod.invoke(builder, env);
56+
Object sessionResult = startSessionMethod.invoke(builder);
57+
58+
if (muteBreakpoints) {
59+
Method getSessionMethod = sessionResult.getClass().getMethod("getSession");
60+
XDebugSession session = (XDebugSession) getSessionMethod.invoke(sessionResult);
61+
session.setBreakpointMuted(true);
62+
}
63+
64+
Method getDescriptorMethod = sessionResult.getClass().getMethod("getRunContentDescriptor");
65+
return (RunContentDescriptor) getDescriptorMethod.invoke(sessionResult);
66+
67+
} catch (NoSuchMethodException e) {
68+
// Fallback to old API for 2025.1 and older
69+
XDebugSession session = manager.startSession(env, starter);
70+
if (muteBreakpoints) {
71+
session.setBreakpointMuted(true);
72+
}
73+
return session.getRunContentDescriptor();
74+
} catch (InvocationTargetException e) {
75+
Throwable cause = e.getCause();
76+
if (cause instanceof ExecutionException) {
77+
throw (ExecutionException) cause;
78+
}
79+
throw new ExecutionException("Failed to start debug session via reflection", cause != null ? cause : e);
80+
} catch (Exception e) {
81+
throw new ExecutionException("Failed with unexpected reflection error", e);
82+
}
83+
}
84+
}

src/io/flutter/run/LaunchState.java

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ protected RunContentDescriptor launch(@NotNull ExecutionEnvironment env) throws
160160
final RunContentDescriptor descriptor;
161161
if (launchMode.supportsDebugConnection()) {
162162
ToolWindowBadgeUpdater.updateBadgedIcon(app, project);
163-
descriptor = createDebugSession(env, app, result).getRunContentDescriptor();
163+
descriptor = createDebugSession(env, app, result);
164164
}
165165
else {
166166
descriptor = new RunContentBuilder(result, env).showRunContent(env.getContentToReuse());
@@ -224,28 +224,22 @@ protected void showNoDeviceConnectedMessage(Project project) {
224224
}
225225

226226
@NotNull
227-
protected XDebugSession createDebugSession(@NotNull final ExecutionEnvironment env,
228-
@NotNull final FlutterApp app,
229-
@NotNull final ExecutionResult executionResult)
227+
protected RunContentDescriptor createDebugSession(@NotNull final ExecutionEnvironment env,
228+
@NotNull final FlutterApp app,
229+
@NotNull final ExecutionResult executionResult)
230230
throws ExecutionException {
231231

232232
final DartUrlResolver resolver = DartUrlResolver.getInstance(env.getProject(), sourceLocation);
233233
final FlutterPositionMapper mapper = createPositionMapper(env, app, resolver);
234234

235235
final XDebuggerManager manager = XDebuggerManager.getInstance(env.getProject());
236-
final XDebugSession session = manager.startSession(env, new XDebugProcessStarter() {
236+
return FlutterDebugSessionUtils.startSessionAndGetDescriptor(manager, env, new XDebugProcessStarter() {
237237
@Override
238238
@NotNull
239239
public XDebugProcess start(@NotNull final XDebugSession session) {
240240
return new FlutterDebugProcess(app, env, session, executionResult, resolver, mapper);
241241
}
242-
});
243-
244-
if (app.getMode() != RunMode.DEBUG) {
245-
session.setBreakpointMuted(true);
246-
}
247-
248-
return session;
242+
}, app.getMode() != RunMode.DEBUG);
249243
}
250244

251245
@NotNull

src/io/flutter/run/bazelTest/BazelTestRunner.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import io.flutter.bazel.WorkspaceCache;
4646
import io.flutter.logging.PluginLogger;
4747
import io.flutter.run.FlutterPositionMapper;
48+
import io.flutter.run.FlutterDebugSessionUtils;
4849
import io.flutter.run.common.CommonTestConfigUtils;
4950
import io.flutter.run.test.FlutterTestRunner;
5051
import io.flutter.settings.FlutterSettings;
@@ -100,15 +101,13 @@ protected RunContentDescriptor runInDebugger(@NotNull BazelTestLaunchState launc
100101

101102
// Create the debug session.
102103
final XDebuggerManager manager = XDebuggerManager.getInstance(env.getProject());
103-
final XDebugSession session = manager.startSession(env, new XDebugProcessStarter() {
104+
return FlutterDebugSessionUtils.startSessionAndGetDescriptor(manager, env, new XDebugProcessStarter() {
104105
@Override
105106
@NotNull
106107
public XDebugProcess start(@NotNull final XDebugSession session) {
107108
return new BazelTestDebugProcess(env, session, executionResult, resolver, connector, mapper);
108109
}
109-
});
110-
111-
return session.getRunContentDescriptor();
110+
}, false);
112111
}
113112

114113
/**

src/io/flutter/run/test/FlutterTestRunner.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import com.intellij.xdebugger.XDebuggerManager;
3434
import com.jetbrains.lang.dart.util.DartUrlResolver;
3535
import io.flutter.FlutterUtils;
36+
import io.flutter.run.FlutterDebugSessionUtils;
3637
import io.flutter.ObservatoryConnector;
3738
import io.flutter.logging.PluginLogger;
3839
import io.flutter.run.FlutterPositionMapper;
@@ -210,15 +211,13 @@ protected RunContentDescriptor runInDebugger(@NotNull TestLaunchState launcher,
210211

211212
// Create the debug session.
212213
final XDebuggerManager manager = XDebuggerManager.getInstance(env.getProject());
213-
final XDebugSession session = manager.startSession(env, new XDebugProcessStarter() {
214+
return FlutterDebugSessionUtils.startSessionAndGetDescriptor(manager, env, new XDebugProcessStarter() {
214215
@Override
215216
@NotNull
216217
public XDebugProcess start(@NotNull final XDebugSession session) {
217218
return new TestDebugProcess(env, session, executionResult, resolver, connector, mapper);
218219
}
219-
});
220-
221-
return session.getRunContentDescriptor();
220+
}, false);
222221
}
223222

224223
/**

0 commit comments

Comments
 (0)