From 8c4e79d334c287d56a627c74a3475403aab07c4f Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Thu, 8 May 2025 12:35:02 -0700 Subject: [PATCH 01/11] Only start DevTools server once --- .../deeplinks/DeepLinksViewFactory.java | 16 +- .../DevToolsExtensionsViewFactory.java | 16 +- .../RemainingDevToolsViewFactory.java | 17 +- .../PropertyEditorViewFactory.java | 12 +- .../run/daemon/DevToolsServerTask.java | 356 ++++++++++++++++++ .../flutter/run/daemon/DevToolsService.java | 291 ++------------ .../src/io/flutter/view/ViewUtils.java | 24 +- 7 files changed, 435 insertions(+), 297 deletions(-) create mode 100644 flutter-idea/src/io/flutter/run/daemon/DevToolsServerTask.java diff --git a/flutter-idea/src/io/flutter/deeplinks/DeepLinksViewFactory.java b/flutter-idea/src/io/flutter/deeplinks/DeepLinksViewFactory.java index b7048a08e6..0b27037454 100644 --- a/flutter-idea/src/io/flutter/deeplinks/DeepLinksViewFactory.java +++ b/flutter-idea/src/io/flutter/deeplinks/DeepLinksViewFactory.java @@ -18,6 +18,7 @@ import io.flutter.sdk.FlutterSdkVersion; import io.flutter.utils.AsyncUtils; import io.flutter.utils.OpenApiUtils; +import io.flutter.view.ViewUtils; import kotlin.coroutines.Continuation; import org.jetbrains.annotations.NotNull; @@ -27,6 +28,9 @@ public class DeepLinksViewFactory implements ToolWindowFactory { @NotNull private static String TOOL_WINDOW_ID = "Flutter Deep Links"; + @NotNull + private final ViewUtils viewUtils = new ViewUtils(); + @Override public Object isApplicableAsync(@NotNull Project project, @NotNull Continuation $completion) { FlutterSdk sdk = FlutterSdk.getFlutterSdk(project); @@ -42,16 +46,8 @@ public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindo AsyncUtils.whenCompleteUiThread( DevToolsService.getInstance(project).getDevToolsInstance(), (instance, error) -> { - // Skip displaying if the project has been closed. - if (!project.isOpen()) { - return; - } - - if (error != null) { - return; - } - - if (instance == null) { + final boolean inValidState = viewUtils.checkDevToolsPanelInValidState(toolWindow, project, instance, error); + if (!inValidState) { return; } diff --git a/flutter-idea/src/io/flutter/devtools/DevToolsExtensionsViewFactory.java b/flutter-idea/src/io/flutter/devtools/DevToolsExtensionsViewFactory.java index 1c65bae261..576b25dd24 100644 --- a/flutter-idea/src/io/flutter/devtools/DevToolsExtensionsViewFactory.java +++ b/flutter-idea/src/io/flutter/devtools/DevToolsExtensionsViewFactory.java @@ -18,6 +18,7 @@ import io.flutter.utils.AsyncUtils; import io.flutter.utils.OpenApiUtils; import io.flutter.view.FlutterViewMessages; +import io.flutter.view.ViewUtils; import kotlin.coroutines.Continuation; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -28,6 +29,9 @@ public class DevToolsExtensionsViewFactory implements ToolWindowFactory { @NotNull private static String TOOL_WINDOW_ID = "Flutter DevTools Extensions"; + @NotNull + private final ViewUtils viewUtils = new ViewUtils(); + public static void init(Project project) { project.getMessageBus().connect().subscribe( FlutterViewMessages.FLUTTER_DEBUG_TOPIC, (FlutterViewMessages.FlutterDebugNotifier)event -> initView(project, event) @@ -51,16 +55,8 @@ public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindo AsyncUtils.whenCompleteUiThread( DevToolsService.getInstance(project).getDevToolsInstance(), (instance, error) -> { - // Skip displaying if the project has been closed. - if (!project.isOpen()) { - return; - } - - if (error != null) { - return; - } - - if (instance == null) { + final boolean inValidState = viewUtils.checkDevToolsPanelInValidState(window, project, instance, error); + if (!inValidState) { return; } diff --git a/flutter-idea/src/io/flutter/devtools/RemainingDevToolsViewFactory.java b/flutter-idea/src/io/flutter/devtools/RemainingDevToolsViewFactory.java index 62adf042bc..3f832f3fd4 100644 --- a/flutter-idea/src/io/flutter/devtools/RemainingDevToolsViewFactory.java +++ b/flutter-idea/src/io/flutter/devtools/RemainingDevToolsViewFactory.java @@ -18,6 +18,7 @@ import io.flutter.utils.AsyncUtils; import io.flutter.utils.OpenApiUtils; import io.flutter.view.FlutterViewMessages; +import io.flutter.view.ViewUtils; import kotlin.coroutines.Continuation; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -28,6 +29,9 @@ public class RemainingDevToolsViewFactory implements ToolWindowFactory { @NotNull private static String TOOL_WINDOW_ID = "Flutter DevTools"; + @NotNull + private final ViewUtils viewUtils = new ViewUtils(); + public static void init(Project project) { project.getMessageBus().connect().subscribe( FlutterViewMessages.FLUTTER_DEBUG_TOPIC, (FlutterViewMessages.FlutterDebugNotifier)event -> initView(project, event) @@ -41,7 +45,6 @@ private static void initView(Project project, FlutterViewMessages.FlutterDebugEv service.updateVmServiceUri(vmServiceUri); } - @Override public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow window) { final ContentManager contentManager = window.getContentManager(); @@ -52,16 +55,8 @@ public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindo AsyncUtils.whenCompleteUiThread( DevToolsService.getInstance(project).getDevToolsInstance(), (instance, error) -> { - // Skip displaying if the project has been closed. - if (!project.isOpen()) { - return; - } - - if (error != null) { - return; - } - - if (instance == null) { + final boolean inValidState = viewUtils.checkDevToolsPanelInValidState(window, project, instance, error); + if (!inValidState) { return; } diff --git a/flutter-idea/src/io/flutter/propertyeditor/PropertyEditorViewFactory.java b/flutter-idea/src/io/flutter/propertyeditor/PropertyEditorViewFactory.java index a6cbf4b7f2..50c7557a38 100644 --- a/flutter-idea/src/io/flutter/propertyeditor/PropertyEditorViewFactory.java +++ b/flutter-idea/src/io/flutter/propertyeditor/PropertyEditorViewFactory.java @@ -54,16 +54,8 @@ public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindo AsyncUtils.whenCompleteUiThread( DevToolsService.getInstance(project).getDevToolsInstance(), (instance, error) -> { - // Skip displaying if the project has been closed. - if (!project.isOpen()) { - return; - } - - if (error != null) { - return; - } - - if (instance == null) { + final boolean inValidState = viewUtils.checkDevToolsPanelInValidState(toolWindow, project, instance, error); + if (!inValidState) { return; } diff --git a/flutter-idea/src/io/flutter/run/daemon/DevToolsServerTask.java b/flutter-idea/src/io/flutter/run/daemon/DevToolsServerTask.java new file mode 100644 index 0000000000..ec04a87cb5 --- /dev/null +++ b/flutter-idea/src/io/flutter/run/daemon/DevToolsServerTask.java @@ -0,0 +1,356 @@ +/* + * Copyright 2025 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +package io.flutter.run.daemon; + +import com.google.common.collect.ImmutableList; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; +import com.intellij.execution.ExecutionException; +import com.intellij.execution.configurations.GeneralCommandLine; +import com.intellij.execution.process.ProcessAdapter; +import com.intellij.execution.process.ProcessEvent; +import com.intellij.execution.process.ProcessHandler; +import com.intellij.notification.Notification; +import com.intellij.notification.NotificationType; +import com.intellij.notification.Notifications; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.Task; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectManager; +import com.intellij.openapi.project.ProjectManagerListener; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.Version; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.registry.Registry; +import com.jetbrains.lang.dart.ide.devtools.DartDevToolsService; +import com.jetbrains.lang.dart.ide.toolingDaemon.DartToolingDaemonService; +import com.jetbrains.lang.dart.sdk.DartSdk; +import io.flutter.FlutterMessages; +import io.flutter.FlutterUtils; +import io.flutter.bazel.Workspace; +import io.flutter.bazel.WorkspaceCache; +import io.flutter.dart.DtdUtils; +import io.flutter.sdk.FlutterSdk; +import io.flutter.sdk.FlutterSdkUtil; +import io.flutter.utils.JsonUtils; +import io.flutter.utils.MostlySilentColoredProcessHandler; +import io.flutter.utils.OpenApiUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; + +class DevToolsServerTask extends Task.Backgroundable { + private static final Logger LOG = Logger.getInstance(DevToolsServerTask.class); + public static final String LOCAL_DEVTOOLS_DIR = "flutter.local.devtools.dir"; + public static final String LOCAL_DEVTOOLS_ARGS = "flutter.local.devtools.args"; + public @Nullable String failureMessage = null; + private @NotNull Project project; + private final AtomicReference> devToolsFutureRef; + + private DaemonApi daemonApi; + private ProcessHandler process; + + public DevToolsServerTask(@NotNull Project project, @NotNull String title, AtomicReference> devToolsFutureRef) { + super(project, title, true); + this.project = project; + this.devToolsFutureRef = devToolsFutureRef; + } + + @Override + public void run(@NotNull ProgressIndicator progressIndicator) { + progressIndicator.setFraction(30); + progressIndicator.setText2("Init"); + + final FlutterSdk sdk = FlutterSdk.getFlutterSdk(project); + boolean dartDevToolsSupported = false; + final DartSdk dartSdk = DartSdk.getDartSdk(project); + if (dartSdk != null) { + final Version version = Version.parseVersion(dartSdk.getVersion()); + assert version != null; + dartDevToolsSupported = version.compareTo(2, 15, 0) >= 0; + } + + if (dartDevToolsSupported) { + // This condition means we can use `dart devtools` to start. + final WorkspaceCache workspaceCache = WorkspaceCache.getInstance(project); + if (workspaceCache.isBazel()) { + // This is only for internal usages. + progressIndicator.setFraction(60); + progressIndicator.setText2("Running server"); + setUpWithDart(createCommand(workspaceCache.get().getRoot().getPath(), workspaceCache.get().getDevToolsScript(), + ImmutableList.of("--machine"))); + } + else { + final String localDevToolsDir = Registry.stringValue(LOCAL_DEVTOOLS_DIR); + if (!localDevToolsDir.isEmpty()) { + // This is only for development to check integration with a locally run DevTools server. + // To enable, follow the instructions in: + // https://github.com/flutter/flutter-intellij/blob/master/CONTRIBUTING.md#developing-with-local-devtools + final DtdUtils dtdUtils = new DtdUtils(); + try { + progressIndicator.setFraction(60); + progressIndicator.setText2("Local server"); + final DartToolingDaemonService dtdService = dtdUtils.readyDtdService(project).get(); + final String dtdUri = dtdService.getUri(); + + final List args = new ArrayList<>(); + args.add("serve"); + args.add("--machine"); + args.add("--dtd-uri=" + dtdUri); + final String localDevToolsArgs = Registry.stringValue(LOCAL_DEVTOOLS_ARGS); + if (!localDevToolsArgs.isEmpty()) { + args.addAll(Arrays.stream(localDevToolsArgs.split(" ")).toList()); + } + setUpInDevMode(createCommand(localDevToolsDir, "dt", args)); + } + catch (InterruptedException | java.util.concurrent.ExecutionException e) { + throw new RuntimeException(e); + } + return; + } + + // The Dart plugin should start DevTools with DTD, so try to use this instance of DevTools before trying to start another. + final String dartPluginUri = DartDevToolsService.getInstance(project).getDevToolsHostAndPort(); + if (dartPluginUri != null) { + String[] parts = dartPluginUri.split(":"); + String host = parts[0]; + Integer port = Integer.parseInt(parts[1]); + if (host != null && port != null) { + devToolsFutureRef.get().complete(new DevToolsInstance(host, port)); + return; + } + } + + setUpWithDart(createCommand(DartSdk.getDartSdk(project).getHomePath(), + DartSdk.getDartSdk(project).getHomePath() + + File.separatorChar + + "bin" + + File.separatorChar + + "dart", + ImmutableList.of("devtools", "--machine"))); + } + } + else { + progressIndicator.setFraction(60); + progressIndicator.setText2("Local server"); + setUpWithDaemon(); + } + } + + @Override + public void onCancel() { + super.onCancel(); + devToolsFutureRef.get().completeExceptionally(new Exception("DevTools start-up canceled.")); + maybeShowErrorNotification(); + } + + @Override + public void onThrowable(@NotNull Throwable error) { + super.onThrowable(error); + cancelTask(new Exception(error)); + } + + private void setUpInDevMode(GeneralCommandLine command) { + try { + this.process = new MostlySilentColoredProcessHandler(command); + this.process.addProcessListener(new ProcessAdapter() { + @Override + public void onTextAvailable(@NotNull ProcessEvent event, @NotNull Key outputType) { + final String text = event.getText().trim(); + + // Keep this printout so developers can see DevTools startup output in idea.log. + System.out.println("DevTools startup: " + text); + tryParseStartupText(text); + } + }); + process.startNotify(); + } + catch (ExecutionException e) { + cancelTask(e); + } + } + + private void setUpWithDart(GeneralCommandLine command) { + try { + this.process = new MostlySilentColoredProcessHandler(command); + this.process.addProcessListener(new ProcessAdapter() { + @Override + public void onTextAvailable(@NotNull ProcessEvent event, @NotNull Key outputType) { + tryParseStartupText(event.getText().trim()); + } + }); + process.startNotify(); + + ProjectManager.getInstance().addProjectManagerListener(project, new ProjectManagerListener() { + @Override + public void projectClosing(@NotNull Project project) { + devToolsFutureRef.set(null); + process.destroyProcess(); + } + }); + } + catch (ExecutionException e) { + cancelTask(e); + } + } + + private void setUpWithDaemon() { + try { + final GeneralCommandLine command = chooseCommand(project); + if (command == null) { + cancelTask("Unable to find daemon command for project"); + return; + } + this.process = new MostlySilentColoredProcessHandler(command); + daemonApi = new DaemonApi(process); + daemonApi.listen(process, new DevToolsService.DevToolsServiceListener()); + daemonApi.devToolsServe().thenAccept((DaemonApi.DevToolsAddress address) -> { + if (!project.isOpen()) { + // We should skip starting DevTools (and doing any UI work) if the project has been closed. + return; + } + if (address == null) { + cancelTask("DevTools address was null"); + } + else { + devToolsFutureRef.get().complete(new DevToolsInstance(address.host, address.port)); + } + }); + } + catch (ExecutionException e) { + cancelTask(e); + } + + ProjectManager.getInstance().addProjectManagerListener(project, new ProjectManagerListener() { + @Override + public void projectClosing(@NotNull Project project) { + devToolsFutureRef.set(null); + + try { + daemonApi.daemonShutdown().get(5, TimeUnit.SECONDS); + } + catch (InterruptedException | java.util.concurrent.ExecutionException | TimeoutException e) { + LOG.error("DevTools daemon did not shut down normally: " + e); + if (!process.isProcessTerminated()) { + process.destroyProcess(); + } + } + } + }); + } + + private void tryParseStartupText(@NotNull String text) { + if (text.startsWith("{") && text.endsWith("}")) { + try { + final JsonElement element = JsonUtils.parseString(text); + + final JsonObject obj = element.getAsJsonObject(); + + if (Objects.equals(JsonUtils.getStringMember(obj, "event"), "server.started")) { + final JsonObject params = obj.getAsJsonObject("params"); + final String host = JsonUtils.getStringMember(params, "host"); + final int port = JsonUtils.getIntMember(params, "port"); + + if (port != -1) { + devToolsFutureRef.get().complete(new DevToolsInstance(host, port)); + } + else { + cancelTask("DevTools port was invalid"); + } + } + } + catch (JsonSyntaxException e) { + cancelTask(e); + } + } + } + + private static GeneralCommandLine chooseCommand(@NotNull final Project project) { + // Use daemon script if this is a bazel project. + final Workspace workspace = WorkspaceCache.getInstance(project).get(); + if (workspace != null) { + final String script = workspace.getDaemonScript(); + if (script != null) { + return createCommand(workspace.getRoot().getPath(), script, ImmutableList.of()); + } + } + + // Otherwise, use the Flutter SDK. + final FlutterSdk sdk = FlutterSdk.getFlutterSdk(project); + if (sdk == null) { + return null; + } + + try { + final String path = FlutterSdkUtil.pathToFlutterTool(sdk.getHomePath()); + return createCommand(sdk.getHomePath(), path, ImmutableList.of("daemon")); + } + catch (ExecutionException e) { + FlutterUtils.warn(LOG, "Unable to calculate command to start Flutter daemon", e); + return null; + } + } + + private static GeneralCommandLine createCommand(String workDir, String command, List arguments) { + final GeneralCommandLine result = new GeneralCommandLine().withWorkDirectory(workDir); + result.setCharset(StandardCharsets.UTF_8); + result.setExePath(FileUtil.toSystemDependentName(command)); + result.withEnvironment(FlutterSdkUtil.FLUTTER_HOST_ENV, (new FlutterSdkUtil()).getFlutterHostEnvValue()); + + for (String argument : arguments) { + result.addParameter(argument); + } + + return result; + } + + private void cancelTask(String message) { + cancelTask(new Exception(message)); + } + + private void cancelTask(Exception exception) { + FlutterUtils.warn(LOG, "DevTools server start-up canceled.", exception); + failureMessage = exception.getMessage(); + throw new ProcessCanceledException(exception); + } + + private void maybeShowErrorNotification() { + // If there is no failure message, this means the user canceled the task themselves. Therefore, don't display an error. + if (failureMessage == null) { + return; + } + + OpenApiUtils.safeInvokeLater(() -> { + final Notification notification = new Notification(FlutterMessages.FLUTTER_NOTIFICATION_GROUP_ID, + "DevTools", + "DevTools failed to start with error: " + failureMessage, + NotificationType.WARNING); + + notification.addAction(new AnAction("Dismiss") { + @Override + public void actionPerformed(@NotNull AnActionEvent event) { + notification.expire(); + } + }); + Notifications.Bus.notify(notification, project); + }); + } +} diff --git a/flutter-idea/src/io/flutter/run/daemon/DevToolsService.java b/flutter-idea/src/io/flutter/run/daemon/DevToolsService.java index 4a27890a8a..8e08fffaa7 100644 --- a/flutter-idea/src/io/flutter/run/daemon/DevToolsService.java +++ b/flutter-idea/src/io/flutter/run/daemon/DevToolsService.java @@ -5,62 +5,32 @@ */ package io.flutter.run.daemon; -import com.google.common.collect.ImmutableList; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonSyntaxException; -import com.intellij.execution.ExecutionException; -import com.intellij.execution.configurations.GeneralCommandLine; -import com.intellij.execution.process.ProcessAdapter; -import com.intellij.execution.process.ProcessEvent; -import com.intellij.execution.process.ProcessHandler; import com.intellij.execution.process.ProcessOutput; -import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator; import com.intellij.openapi.project.Project; -import com.intellij.openapi.project.ProjectManager; -import com.intellij.openapi.project.ProjectManagerListener; -import com.intellij.openapi.util.Key; -import com.intellij.openapi.util.Version; -import com.intellij.openapi.util.io.FileUtil; -import com.intellij.openapi.util.registry.Registry; -import com.jetbrains.lang.dart.ide.devtools.DartDevToolsService; -import com.jetbrains.lang.dart.ide.toolingDaemon.DartToolingDaemonService; -import com.jetbrains.lang.dart.sdk.DartSdk; -import io.flutter.FlutterUtils; -import io.flutter.bazel.Workspace; -import io.flutter.bazel.WorkspaceCache; import io.flutter.console.FlutterConsoles; -import io.flutter.dart.DtdUtils; import io.flutter.sdk.FlutterCommand; import io.flutter.sdk.FlutterSdk; -import io.flutter.sdk.FlutterSdkUtil; -import io.flutter.utils.JsonUtils; -import io.flutter.utils.MostlySilentColoredProcessHandler; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -import java.io.File; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; import java.util.Objects; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; public class DevToolsService { private static final Logger LOG = Logger.getInstance(DevToolsService.class); - public static final String LOCAL_DEVTOOLS_DIR = "flutter.local.devtools.dir"; - public static final String LOCAL_DEVTOOLS_ARGS = "flutter.local.devtools.args"; - - private static class DevToolsServiceListener implements DaemonEvent.Listener { + protected static class DevToolsServiceListener implements DaemonEvent.Listener { } @NotNull private final Project project; - private DaemonApi daemonApi; - private ProcessHandler process; + + @Nullable private DevToolsServerTask devToolsServerTask; + + @Nullable private BackgroundableProcessIndicator devToolsServerProgressIndicator; + private AtomicReference> devToolsFutureRef = new AtomicReference<>(null); @NotNull @@ -105,203 +75,52 @@ public CompletableFuture getDevToolsInstanceWithForcedRestart( if (futureInstance == null) { devToolsFutureRef.set(new CompletableFuture<>()); - startServer(); + startServer(true); } else if (!futureInstance.isDone()) { futureInstance.cancel(true); devToolsFutureRef.set(new CompletableFuture<>()); - startServer(); + startServer(true); } return devToolsFutureRef.get(); } private void startServer() { - ApplicationManager.getApplication().executeOnPooledThread(() -> { - final FlutterSdk sdk = FlutterSdk.getFlutterSdk(project); + startServer(false); + } - boolean dartDevToolsSupported = false; - final DartSdk dartSdk = DartSdk.getDartSdk(project); - if (dartSdk != null) { - final Version version = Version.parseVersion(dartSdk.getVersion()); - assert version != null; - dartDevToolsSupported = version.compareTo(2, 15, 0) >= 0; + private void startServer(boolean forceRestart) { + if (forceRestart) { + // If this is a force-restart request and the previous DevTools server is still running, cancel it before starting another. + if (devToolsServerProgressIndicator != null && devToolsServerProgressIndicator.isRunning()) { + devToolsServerProgressIndicator.cancel(); } - - if (dartDevToolsSupported) { - // This condition means we can use `dart devtools` to start. - final WorkspaceCache workspaceCache = WorkspaceCache.getInstance(project); - if (workspaceCache.isBazel()) { - // This is only for internal usages. - setUpWithDart(createCommand(workspaceCache.get().getRoot().getPath(), workspaceCache.get().getDevToolsScript(), - ImmutableList.of("--machine"))); - } - else { - final String localDevToolsDir = Registry.stringValue(LOCAL_DEVTOOLS_DIR); - if (!localDevToolsDir.isEmpty()) { - // This is only for development to check integration with a locally run DevTools server. - // To enable, follow the instructions in: - // https://github.com/flutter/flutter-intellij/blob/master/CONTRIBUTING.md#developing-with-local-devtools - final DtdUtils dtdUtils = new DtdUtils(); - try { - final DartToolingDaemonService dtdService = dtdUtils.readyDtdService(project).get(); - final String dtdUri = dtdService.getUri(); - - final List args = new ArrayList<>(); - args.add("serve"); - args.add("--machine"); - args.add("--dtd-uri=" + dtdUri); - final String localDevToolsArgs = Registry.stringValue(LOCAL_DEVTOOLS_ARGS); - if (!localDevToolsArgs.isEmpty()) { - args.addAll(Arrays.stream(localDevToolsArgs.split(" ")).toList()); - } - - setUpInDevMode(createCommand(localDevToolsDir, "dt", args)); - } - catch (InterruptedException | java.util.concurrent.ExecutionException e) { - throw new RuntimeException(e); - } - return; - } - - // The Dart plugin should start DevTools with DTD, so try to use this instance of DevTools before trying to start another. - final String dartPluginUri = DartDevToolsService.getInstance(project).getDevToolsHostAndPort(); - if (dartPluginUri != null) { - String[] parts = dartPluginUri.split(":"); - String host = parts[0]; - Integer port = Integer.parseInt(parts[1]); - if (host != null && port != null) { - devToolsFutureRef.get().complete(new DevToolsInstance(host, port)); - return; - } - } - - setUpWithDart(createCommand(DartSdk.getDartSdk(project).getHomePath(), - DartSdk.getDartSdk(project).getHomePath() + File.separatorChar + "bin" + File.separatorChar + "dart", - ImmutableList.of("devtools", "--machine"))); + } else { + // If this is not a force-restart request, do not start a new DevTools server if one is already running, or if we have a + // DevTools instance. + if (devToolsServerProgressIndicator != null) { + if (devToolsServerProgressIndicator.isRunning() || devToolsInstanceExists()) { + return; } } - else { - setUpWithDaemon(); - } - }); - } - - private void setUpInDevMode(GeneralCommandLine command) { - try { - this.process = new MostlySilentColoredProcessHandler(command); - this.process.addProcessListener(new ProcessAdapter() { - @Override - public void onTextAvailable(@NotNull ProcessEvent event, @NotNull Key outputType) { - final String text = event.getText().trim(); - - // Keep this printout so developers can see DevTools startup output in idea.log. - System.out.println("DevTools startup: " + text); - tryParseStartupText(text); - } - }); - process.startNotify(); - } - catch (ExecutionException e) { - logExceptionAndComplete(e); } - } - - private void setUpWithDart(GeneralCommandLine command) { - try { - this.process = new MostlySilentColoredProcessHandler(command); - this.process.addProcessListener(new ProcessAdapter() { - @Override - public void onTextAvailable(@NotNull ProcessEvent event, @NotNull Key outputType) { - tryParseStartupText(event.getText().trim()); - } - }); - process.startNotify(); - - ProjectManager.getInstance().addProjectManagerListener(project, new ProjectManagerListener() { - @Override - public void projectClosing(@NotNull Project project) { - devToolsFutureRef.set(null); - process.destroyProcess(); - } - }); - } - catch (ExecutionException e) { - logExceptionAndComplete(e); - } - } - - private void tryParseStartupText(@NotNull String text) { - if (text.startsWith("{") && text.endsWith("}")) { - try { - final JsonElement element = JsonUtils.parseString(text); - - final JsonObject obj = element.getAsJsonObject(); - - if (Objects.equals(JsonUtils.getStringMember(obj, "event"), "server.started")) { - final JsonObject params = obj.getAsJsonObject("params"); - final String host = JsonUtils.getStringMember(params, "host"); - final int port = JsonUtils.getIntMember(params, "port"); - if (port != -1) { - devToolsFutureRef.get().complete(new DevToolsInstance(host, port)); - } - else { - logExceptionAndComplete("DevTools port was invalid"); - } - } - } - catch (JsonSyntaxException e) { - logExceptionAndComplete(e); - } - } + // Start the DevTools server. + devToolsServerTask = new DevToolsServerTask(project, "Starting DevTools", devToolsFutureRef); + devToolsServerProgressIndicator = new BackgroundableProcessIndicator(project, devToolsServerTask); + ProgressManager.getInstance() + .runProcessWithProgressAsynchronously( + devToolsServerTask, devToolsServerProgressIndicator); } - private void setUpWithDaemon() { - try { - final GeneralCommandLine command = chooseCommand(project); - if (command == null) { - logExceptionAndComplete("Unable to find daemon command for project"); - return; - } - this.process = new MostlySilentColoredProcessHandler(command); - daemonApi = new DaemonApi(process); - daemonApi.listen(process, new DevToolsServiceListener()); - daemonApi.devToolsServe().thenAccept((DaemonApi.DevToolsAddress address) -> { - if (!project.isOpen()) { - // We should skip starting DevTools (and doing any UI work) if the project has been closed. - return; - } - if (address == null) { - logExceptionAndComplete("DevTools address was null"); - } - else { - devToolsFutureRef.get().complete(new DevToolsInstance(address.host, address.port)); - } - }); - } - catch (ExecutionException e) { - logExceptionAndComplete(e); + private boolean devToolsInstanceExists() { + if (devToolsFutureRef != null) { + final CompletableFuture devToolsFuture = devToolsFutureRef.get(); + return devToolsFuture != null && devToolsFuture.isDone() && !devToolsFuture.isCompletedExceptionally(); } - - ProjectManager.getInstance().addProjectManagerListener(project, new ProjectManagerListener() { - @Override - public void projectClosing(@NotNull Project project) { - devToolsFutureRef.set(null); - - try { - daemonApi.daemonShutdown().get(5, TimeUnit.SECONDS); - } - catch (InterruptedException | java.util.concurrent.ExecutionException | TimeoutException e) { - LOG.error("DevTools daemon did not shut down normally: " + e); - if (!process.isProcessTerminated()) { - process.destroyProcess(); - } - } - } - }); + return false; } - private CompletableFuture pubActivateDevTools(FlutterSdk sdk) { final FlutterCommand command = sdk.flutterPub(null, "global", "activate", "devtools"); @@ -338,44 +157,6 @@ private void logExceptionAndComplete(Exception exception) { future.completeExceptionally(exception); } } - - private static GeneralCommandLine chooseCommand(@NotNull final Project project) { - // Use daemon script if this is a bazel project. - final Workspace workspace = WorkspaceCache.getInstance(project).get(); - if (workspace != null) { - final String script = workspace.getDaemonScript(); - if (script != null) { - return createCommand(workspace.getRoot().getPath(), script, ImmutableList.of()); - } - } - - // Otherwise, use the Flutter SDK. - final FlutterSdk sdk = FlutterSdk.getFlutterSdk(project); - if (sdk == null) { - return null; - } - - try { - final String path = FlutterSdkUtil.pathToFlutterTool(sdk.getHomePath()); - return createCommand(sdk.getHomePath(), path, ImmutableList.of("daemon")); - } - catch (ExecutionException e) { - FlutterUtils.warn(LOG, "Unable to calculate command to start Flutter daemon", e); - return null; - } - } - - private static GeneralCommandLine createCommand(String workDir, String command, List arguments) { - final GeneralCommandLine result = new GeneralCommandLine().withWorkDirectory(workDir); - result.setCharset(StandardCharsets.UTF_8); - result.setExePath(FileUtil.toSystemDependentName(command)); - result.withEnvironment(FlutterSdkUtil.FLUTTER_HOST_ENV, (new FlutterSdkUtil()).getFlutterHostEnvValue()); - - for (String argument : arguments) { - result.addParameter(argument); - } - - return result; - } } + diff --git a/flutter-idea/src/io/flutter/view/ViewUtils.java b/flutter-idea/src/io/flutter/view/ViewUtils.java index 8564ace957..74bab737ce 100644 --- a/flutter-idea/src/io/flutter/view/ViewUtils.java +++ b/flutter-idea/src/io/flutter/view/ViewUtils.java @@ -5,6 +5,7 @@ */ package io.flutter.view; +import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.VerticalFlowLayout; import com.intellij.openapi.wm.ToolWindow; import com.intellij.ui.components.JBLabel; @@ -13,6 +14,7 @@ import com.intellij.ui.content.ContentManager; import com.intellij.util.ui.JBUI; import com.intellij.util.ui.UIUtil; +import io.flutter.run.daemon.DevToolsInstance; import io.flutter.utils.LabelInput; import io.flutter.utils.OpenApiUtils; @@ -22,7 +24,7 @@ public class ViewUtils { public void presentLabel(ToolWindow toolWindow, String text) { - final JBLabel label = new JBLabel(text, SwingConstants.CENTER); + final JBLabel label = new JBLabel("" + text + "", SwingConstants.CENTER); label.setForeground(UIUtil.getLabelDisabledForeground()); replacePanelLabel(toolWindow, label); } @@ -51,6 +53,26 @@ public void presentClickableLabel(ToolWindow toolWindow, List labels replacePanelLabel(toolWindow, center); } + public boolean checkDevToolsPanelInValidState(ToolWindow toolWindow, Project project, DevToolsInstance instance, Throwable error) { + if (!project.isOpen()) { + presentLabel(toolWindow, "

Project is not open.

"); + return false; + } + + final String restartDevToolsMessage = "

Try switching to another Flutter panel and back again to re-start the server.

"; + if (error != null) { + presentLabel(toolWindow, "

Flutter DevTools start-up failed.

" + restartDevToolsMessage); + return false; + } + + if (instance == null) { + presentLabel(toolWindow, "

Flutter DevTools does not exist.

" + restartDevToolsMessage); + return false; + } + + return true; + } + private void replacePanelLabel(ToolWindow toolWindow, JComponent label) { OpenApiUtils.safeInvokeLater(() -> { final ContentManager contentManager = toolWindow.getContentManager(); From 3bf0037cd86ab0246c9c4c6fac9b516b32570f06 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Thu, 8 May 2025 12:35:58 -0700 Subject: [PATCH 02/11] Rename helper method for clarity --- flutter-idea/src/io/flutter/deeplinks/DeepLinksViewFactory.java | 2 +- .../src/io/flutter/devtools/DevToolsExtensionsViewFactory.java | 2 +- .../src/io/flutter/devtools/RemainingDevToolsViewFactory.java | 2 +- .../io/flutter/propertyeditor/PropertyEditorViewFactory.java | 2 +- flutter-idea/src/io/flutter/view/ViewUtils.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/flutter-idea/src/io/flutter/deeplinks/DeepLinksViewFactory.java b/flutter-idea/src/io/flutter/deeplinks/DeepLinksViewFactory.java index 0b27037454..af6a102ef6 100644 --- a/flutter-idea/src/io/flutter/deeplinks/DeepLinksViewFactory.java +++ b/flutter-idea/src/io/flutter/deeplinks/DeepLinksViewFactory.java @@ -46,7 +46,7 @@ public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindo AsyncUtils.whenCompleteUiThread( DevToolsService.getInstance(project).getDevToolsInstance(), (instance, error) -> { - final boolean inValidState = viewUtils.checkDevToolsPanelInValidState(toolWindow, project, instance, error); + final boolean inValidState = viewUtils.verifyDevToolsPanelStateIsValid(toolWindow, project, instance, error); if (!inValidState) { return; } diff --git a/flutter-idea/src/io/flutter/devtools/DevToolsExtensionsViewFactory.java b/flutter-idea/src/io/flutter/devtools/DevToolsExtensionsViewFactory.java index 576b25dd24..715e539c50 100644 --- a/flutter-idea/src/io/flutter/devtools/DevToolsExtensionsViewFactory.java +++ b/flutter-idea/src/io/flutter/devtools/DevToolsExtensionsViewFactory.java @@ -55,7 +55,7 @@ public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindo AsyncUtils.whenCompleteUiThread( DevToolsService.getInstance(project).getDevToolsInstance(), (instance, error) -> { - final boolean inValidState = viewUtils.checkDevToolsPanelInValidState(window, project, instance, error); + final boolean inValidState = viewUtils.verifyDevToolsPanelStateIsValid(window, project, instance, error); if (!inValidState) { return; } diff --git a/flutter-idea/src/io/flutter/devtools/RemainingDevToolsViewFactory.java b/flutter-idea/src/io/flutter/devtools/RemainingDevToolsViewFactory.java index 3f832f3fd4..3d27158676 100644 --- a/flutter-idea/src/io/flutter/devtools/RemainingDevToolsViewFactory.java +++ b/flutter-idea/src/io/flutter/devtools/RemainingDevToolsViewFactory.java @@ -55,7 +55,7 @@ public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindo AsyncUtils.whenCompleteUiThread( DevToolsService.getInstance(project).getDevToolsInstance(), (instance, error) -> { - final boolean inValidState = viewUtils.checkDevToolsPanelInValidState(window, project, instance, error); + final boolean inValidState = viewUtils.verifyDevToolsPanelStateIsValid(window, project, instance, error); if (!inValidState) { return; } diff --git a/flutter-idea/src/io/flutter/propertyeditor/PropertyEditorViewFactory.java b/flutter-idea/src/io/flutter/propertyeditor/PropertyEditorViewFactory.java index 50c7557a38..1b353194da 100644 --- a/flutter-idea/src/io/flutter/propertyeditor/PropertyEditorViewFactory.java +++ b/flutter-idea/src/io/flutter/propertyeditor/PropertyEditorViewFactory.java @@ -54,7 +54,7 @@ public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindo AsyncUtils.whenCompleteUiThread( DevToolsService.getInstance(project).getDevToolsInstance(), (instance, error) -> { - final boolean inValidState = viewUtils.checkDevToolsPanelInValidState(toolWindow, project, instance, error); + final boolean inValidState = viewUtils.verifyDevToolsPanelStateIsValid(toolWindow, project, instance, error); if (!inValidState) { return; } diff --git a/flutter-idea/src/io/flutter/view/ViewUtils.java b/flutter-idea/src/io/flutter/view/ViewUtils.java index 74bab737ce..0ca20e596e 100644 --- a/flutter-idea/src/io/flutter/view/ViewUtils.java +++ b/flutter-idea/src/io/flutter/view/ViewUtils.java @@ -53,7 +53,7 @@ public void presentClickableLabel(ToolWindow toolWindow, List labels replacePanelLabel(toolWindow, center); } - public boolean checkDevToolsPanelInValidState(ToolWindow toolWindow, Project project, DevToolsInstance instance, Throwable error) { + public boolean verifyDevToolsPanelStateIsValid(ToolWindow toolWindow, Project project, DevToolsInstance instance, Throwable error) { if (!project.isOpen()) { presentLabel(toolWindow, "

Project is not open.

"); return false; From 4fdf4362cdeb84d697005fbf47e71f6e9289c05c Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Thu, 8 May 2025 15:14:13 -0700 Subject: [PATCH 03/11] Wait for the Dart Plugin to start the DevTools server with retries --- .../run/daemon/DevToolsServerTask.java | 219 +++++++++++------- .../flutter/run/daemon/DevToolsService.java | 2 + 2 files changed, 133 insertions(+), 88 deletions(-) diff --git a/flutter-idea/src/io/flutter/run/daemon/DevToolsServerTask.java b/flutter-idea/src/io/flutter/run/daemon/DevToolsServerTask.java index ec04a87cb5..80d4fc8db8 100644 --- a/flutter-idea/src/io/flutter/run/daemon/DevToolsServerTask.java +++ b/flutter-idea/src/io/flutter/run/daemon/DevToolsServerTask.java @@ -56,6 +56,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; +import java.util.Optional; class DevToolsServerTask extends Task.Backgroundable { private static final Logger LOG = Logger.getInstance(DevToolsServerTask.class); @@ -68,7 +69,9 @@ class DevToolsServerTask extends Task.Backgroundable { private DaemonApi daemonApi; private ProcessHandler process; - public DevToolsServerTask(@NotNull Project project, @NotNull String title, AtomicReference> devToolsFutureRef) { + public DevToolsServerTask(@NotNull Project project, + @NotNull String title, + AtomicReference> devToolsFutureRef) { super(project, title, true); this.project = project; this.devToolsFutureRef = devToolsFutureRef; @@ -76,96 +79,96 @@ public DevToolsServerTask(@NotNull Project project, @NotNull String title, Atomi @Override public void run(@NotNull ProgressIndicator progressIndicator) { - progressIndicator.setFraction(30); - progressIndicator.setText2("Init"); + try { + progressIndicator.setFraction(30); + progressIndicator.setText2("Init"); - final FlutterSdk sdk = FlutterSdk.getFlutterSdk(project); - boolean dartDevToolsSupported = false; - final DartSdk dartSdk = DartSdk.getDartSdk(project); - if (dartSdk != null) { - final Version version = Version.parseVersion(dartSdk.getVersion()); - assert version != null; - dartDevToolsSupported = version.compareTo(2, 15, 0) >= 0; - } + // If DevTools is not supported, start the daemon instead. + final boolean dartDevToolsSupported = dartSdkSupportsDartDevTools(); + if (!dartDevToolsSupported) { + progressIndicator.setFraction(60); + progressIndicator.setText2("Daemon set-up"); + setUpWithDaemon(); + return; + } - if (dartDevToolsSupported) { - // This condition means we can use `dart devtools` to start. + // If we are in a Bazel workspace, start the server. + // Note: This is only for internal usages. final WorkspaceCache workspaceCache = WorkspaceCache.getInstance(project); if (workspaceCache.isBazel()) { - // This is only for internal usages. progressIndicator.setFraction(60); - progressIndicator.setText2("Running server"); + progressIndicator.setText2("Starting server"); setUpWithDart(createCommand(workspaceCache.get().getRoot().getPath(), workspaceCache.get().getDevToolsScript(), ImmutableList.of("--machine"))); + return; } - else { - final String localDevToolsDir = Registry.stringValue(LOCAL_DEVTOOLS_DIR); - if (!localDevToolsDir.isEmpty()) { - // This is only for development to check integration with a locally run DevTools server. - // To enable, follow the instructions in: - // https://github.com/flutter/flutter-intellij/blob/master/CONTRIBUTING.md#developing-with-local-devtools - final DtdUtils dtdUtils = new DtdUtils(); - try { - progressIndicator.setFraction(60); - progressIndicator.setText2("Local server"); - final DartToolingDaemonService dtdService = dtdUtils.readyDtdService(project).get(); - final String dtdUri = dtdService.getUri(); - - final List args = new ArrayList<>(); - args.add("serve"); - args.add("--machine"); - args.add("--dtd-uri=" + dtdUri); - final String localDevToolsArgs = Registry.stringValue(LOCAL_DEVTOOLS_ARGS); - if (!localDevToolsArgs.isEmpty()) { - args.addAll(Arrays.stream(localDevToolsArgs.split(" ")).toList()); - } - setUpInDevMode(createCommand(localDevToolsDir, "dt", args)); - } - catch (InterruptedException | java.util.concurrent.ExecutionException e) { - throw new RuntimeException(e); - } - return; - } - // The Dart plugin should start DevTools with DTD, so try to use this instance of DevTools before trying to start another. - final String dartPluginUri = DartDevToolsService.getInstance(project).getDevToolsHostAndPort(); - if (dartPluginUri != null) { - String[] parts = dartPluginUri.split(":"); - String host = parts[0]; - Integer port = Integer.parseInt(parts[1]); - if (host != null && port != null) { - devToolsFutureRef.get().complete(new DevToolsInstance(host, port)); - return; - } - } + // This is only for development to check integration with a locally run DevTools server. + // To enable, follow the instructions in: + // https://github.com/flutter/flutter-intellij/blob/master/CONTRIBUTING.md#developing-with-local-devtools + final String localDevToolsDir = Registry.stringValue(LOCAL_DEVTOOLS_DIR); + if (!localDevToolsDir.isEmpty()) { + progressIndicator.setFraction(60); + progressIndicator.setText2("Starting local server"); + setUpLocalServer(localDevToolsDir); + } + // If the Dart plugin does not start DevTools, then call `dart devtools` to start the server. + final Boolean dartPluginStartsDevTools = true; + if (!dartPluginStartsDevTools) { + progressIndicator.setFraction(60); + progressIndicator.setText2("Starting server"); setUpWithDart(createCommand(DartSdk.getDartSdk(project).getHomePath(), - DartSdk.getDartSdk(project).getHomePath() + - File.separatorChar + - "bin" + - File.separatorChar + - "dart", + DartSdk.getDartSdk(project).getHomePath() + File.separatorChar + "bin" + File.separatorChar + "dart", ImmutableList.of("devtools", "--machine"))); } + + final Optional devToolsOptional = checkForDartPluginInitiatedDevToolsWithRetries(progressIndicator).get(); + if (devToolsOptional.isEmpty() || devToolsOptional.get() == null) { + System.out.println("DevTools is null!"); + cancelWithError(new TimeoutException("Timed-out waiting for the Dart Plugin to start DevTools.")); + return; + } + + devToolsFutureRef.get().complete(devToolsOptional.get()); } - else { - progressIndicator.setFraction(60); - progressIndicator.setText2("Local server"); - setUpWithDaemon(); + catch (java.util.concurrent.ExecutionException | InterruptedException e) { + cancelWithError(e); + return; } } - @Override - public void onCancel() { - super.onCancel(); - devToolsFutureRef.get().completeExceptionally(new Exception("DevTools start-up canceled.")); - maybeShowErrorNotification(); + private Boolean dartSdkSupportsDartDevTools() { + final DartSdk dartSdk = DartSdk.getDartSdk(project); + if (dartSdk != null) { + final Version version = Version.parseVersion(dartSdk.getVersion()); + assert version != null; + return version.compareTo(2, 15, 0) >= 0; + } + return false; } @Override public void onThrowable(@NotNull Throwable error) { super.onThrowable(error); - cancelTask(new Exception(error)); + cancelWithError(new Exception(error)); + } + + private void setUpLocalServer(String localDevToolsDir) throws java.util.concurrent.ExecutionException, InterruptedException { + final DtdUtils dtdUtils = new DtdUtils(); + final DartToolingDaemonService dtdService = dtdUtils.readyDtdService(project).get(); + final String dtdUri = dtdService.getUri(); + + final List args = new ArrayList<>(); + args.add("serve"); + args.add("--machine"); + args.add("--dtd-uri=" + dtdUri); + final String localDevToolsArgs = Registry.stringValue(LOCAL_DEVTOOLS_ARGS); + if (!localDevToolsArgs.isEmpty()) { + args.addAll(Arrays.stream(localDevToolsArgs.split(" ")).toList()); + } + + setUpInDevMode(createCommand(localDevToolsDir, "dt", args)); } private void setUpInDevMode(GeneralCommandLine command) { @@ -184,7 +187,7 @@ public void onTextAvailable(@NotNull ProcessEvent event, @NotNull Key outputType process.startNotify(); } catch (ExecutionException e) { - cancelTask(e); + cancelWithError(e); } } @@ -208,7 +211,7 @@ public void projectClosing(@NotNull Project project) { }); } catch (ExecutionException e) { - cancelTask(e); + cancelWithError(e); } } @@ -216,7 +219,7 @@ private void setUpWithDaemon() { try { final GeneralCommandLine command = chooseCommand(project); if (command == null) { - cancelTask("Unable to find daemon command for project"); + cancelWithError("Unable to find daemon command for project"); return; } this.process = new MostlySilentColoredProcessHandler(command); @@ -228,7 +231,7 @@ private void setUpWithDaemon() { return; } if (address == null) { - cancelTask("DevTools address was null"); + cancelWithError("DevTools address was null"); } else { devToolsFutureRef.get().complete(new DevToolsInstance(address.host, address.port)); @@ -236,7 +239,7 @@ private void setUpWithDaemon() { }); } catch (ExecutionException e) { - cancelTask(e); + cancelWithError(e); } ProjectManager.getInstance().addProjectManagerListener(project, new ProjectManagerListener() { @@ -257,6 +260,49 @@ public void projectClosing(@NotNull Project project) { }); } + private CompletableFuture> checkForDartPluginInitiatedDevToolsWithRetries(ProgressIndicator progressIndicator) throws InterruptedException { + progressIndicator.setText2("Waiting for Dart Plugin"); + final CompletableFuture> devToolsFuture = new CompletableFuture<>(); + + final long msBetweenRetries = 1500; + int retries = 10; + while (retries >= 0) { + final double currentProgress = progressIndicator.getFraction(); + progressIndicator.setFraction(Math.min(currentProgress + 10, 95)); + + final @Nullable DevToolsInstance devTools = createDevToolsInstanceFromDartPluginUri(); + if (devTools != null) { + System.out.println("DevTools ready! retry number " + retries); + devToolsFuture.complete(Optional.of(devTools)); + return devToolsFuture; + } + else { + System.out.println("DevTools not ready yet, retry number " + retries); + Thread.sleep(msBetweenRetries); + retries--; + } + } + + devToolsFuture.complete(Optional.ofNullable(null)); + return devToolsFuture; + } + + private @Nullable DevToolsInstance createDevToolsInstanceFromDartPluginUri() { + final String dartPluginUri = DartDevToolsService.getInstance(project).getDevToolsHostAndPort(); + if (dartPluginUri == null) { + return null; + } + + String[] parts = dartPluginUri.split(":"); + String host = parts[0]; + Integer port = Integer.parseInt(parts[1]); + if (host == null || port == null) { + return null; + } + + return new DevToolsInstance(host, port); + } + private void tryParseStartupText(@NotNull String text) { if (text.startsWith("{") && text.endsWith("}")) { try { @@ -273,12 +319,12 @@ private void tryParseStartupText(@NotNull String text) { devToolsFutureRef.get().complete(new DevToolsInstance(host, port)); } else { - cancelTask("DevTools port was invalid"); + cancelWithError("DevTools port was invalid"); } } } catch (JsonSyntaxException e) { - cancelTask(e); + cancelWithError(e); } } } @@ -322,26 +368,23 @@ private static GeneralCommandLine createCommand(String workDir, String command, return result; } - private void cancelTask(String message) { - cancelTask(new Exception(message)); + private void cancelWithError(String message) { + cancelWithError(new Exception(message)); } - private void cancelTask(Exception exception) { - FlutterUtils.warn(LOG, "DevTools server start-up canceled.", exception); - failureMessage = exception.getMessage(); + private void cancelWithError(Exception exception) { + final String errorTitle = "DevTools server start-up failure."; + FlutterUtils.warn(LOG, errorTitle, exception); + devToolsFutureRef.get().completeExceptionally(new Exception(errorTitle)); + showErrorNotification(errorTitle, exception.getMessage()); throw new ProcessCanceledException(exception); } - private void maybeShowErrorNotification() { - // If there is no failure message, this means the user canceled the task themselves. Therefore, don't display an error. - if (failureMessage == null) { - return; - } - + private void showErrorNotification(String errorTitle, String errorDetails) { OpenApiUtils.safeInvokeLater(() -> { final Notification notification = new Notification(FlutterMessages.FLUTTER_NOTIFICATION_GROUP_ID, "DevTools", - "DevTools failed to start with error: " + failureMessage, + errorTitle + " " + errorDetails, NotificationType.WARNING); notification.addAction(new AnAction("Dismiss") { diff --git a/flutter-idea/src/io/flutter/run/daemon/DevToolsService.java b/flutter-idea/src/io/flutter/run/daemon/DevToolsService.java index 8e08fffaa7..c5dee5fdbb 100644 --- a/flutter-idea/src/io/flutter/run/daemon/DevToolsService.java +++ b/flutter-idea/src/io/flutter/run/daemon/DevToolsService.java @@ -102,6 +102,8 @@ private void startServer(boolean forceRestart) { if (devToolsServerProgressIndicator != null) { if (devToolsServerProgressIndicator.isRunning() || devToolsInstanceExists()) { return; + } else { + devToolsServerProgressIndicator.cancel(); } } } From 93d3143fa4c0e6cbe3b565dab00b8cba9cb5de55 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Fri, 9 May 2025 12:30:30 -0700 Subject: [PATCH 04/11] Add reload logic to AbstractDevToolsViewFactory --- .../devtools/AbstractDevToolsViewFactory.java | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/flutter-idea/src/io/flutter/devtools/AbstractDevToolsViewFactory.java b/flutter-idea/src/io/flutter/devtools/AbstractDevToolsViewFactory.java index e46e528211..0ca69a7d39 100644 --- a/flutter-idea/src/io/flutter/devtools/AbstractDevToolsViewFactory.java +++ b/flutter-idea/src/io/flutter/devtools/AbstractDevToolsViewFactory.java @@ -6,8 +6,11 @@ package io.flutter.devtools; import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Disposer; import com.intellij.openapi.wm.ToolWindow; import com.intellij.openapi.wm.ToolWindowFactory; +import com.intellij.openapi.wm.ex.ToolWindowManagerListener; +import com.intellij.util.messages.MessageBusConnection; import io.flutter.FlutterUtils; import io.flutter.actions.RefreshToolWindowAction; import io.flutter.run.daemon.DevToolsInstance; @@ -44,6 +47,8 @@ public abstract DevToolsUrl getDevToolsUrl(@NotNull Project project, protected void doAfterBrowserOpened(@NotNull Project project, @NotNull EmbeddedBrowser browser) {} + private boolean devToolsLoadedInBrowser = false; + @Override public Object isApplicableAsync(@NotNull Project project, @NotNull Continuation $completion) { // Due to https://github.com/flutter/flutter/issues/142521, this always returns true when the @@ -98,19 +103,36 @@ public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindo } // Final case: + loadDevToolsInEmbeddedBrowser(project, toolWindow, flutterSdkVersion); + + // Finally, listen for the panel to be reopened and potentially reload DevTools. + maybeReloadDevToolsWhenVisible(project, toolWindow, flutterSdkVersion); + } + + private void loadDevToolsInEmbeddedBrowser(@NotNull Project project, + @NotNull ToolWindow toolWindow, + @NotNull FlutterSdkVersion flutterSdkVersion) { AsyncUtils.whenCompleteUiThread( DevToolsService.getInstance(project).getDevToolsInstance(), (instance, error) -> { + viewUtils.presentLabel(toolWindow, "Loading " + getToolWindowTitle() + "..."); + // Skip displaying if the project has been closed. if (!project.isOpen()) { + viewUtils.presentLabel(toolWindow, "Project is closed."); return; } + // Show a message if DevTools started with an error. + final String restartDevToolsMessage = "Try switching to another Flutter panel and back again to restart the server."; if (error != null) { + viewUtils.presentLabels(toolWindow, List.of("Flutter DevTools start-up failed.", restartDevToolsMessage)); return; } + // Show a message if there is no DevTools yet. if (instance == null) { + viewUtils.presentLabels(toolWindow, List.of("Flutter DevTools does not exist.", restartDevToolsMessage)); return; } @@ -122,14 +144,30 @@ public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindo .ifPresent(embeddedBrowser -> { embeddedBrowser.openPanel(toolWindow, getToolWindowTitle(), devToolsUrl, System.out::println); + devToolsLoadedInBrowser = true; doAfterBrowserOpened(project, embeddedBrowser); + // The "refresh" action refreshes the embedded browser, not the panel. + // Therefore, we only show it once we have an embedded browser. + toolWindow.setTitleActions(List.of(new RefreshToolWindowAction(getToolWindowId()))); }); }); } ); + } - // TODO(helin24): It may be better to add this to the gear actions or to attach as a mouse event on individual tabs within a tool - // window, but I wasn't able to get either working immediately. - toolWindow.setTitleActions(List.of(new RefreshToolWindowAction(getToolWindowId()))); + private void maybeReloadDevToolsWhenVisible(@NotNull Project project, + @NotNull ToolWindow toolWindow, @NotNull FlutterSdkVersion flutterSdkVersion) { + MessageBusConnection connection = project.getMessageBus().connect(); + connection.subscribe(ToolWindowManagerListener.TOPIC, new ToolWindowManagerListener() { + @Override + public void toolWindowShown(@NotNull ToolWindow activatedToolWindow) { + if (activatedToolWindow.getId().equals(getToolWindowId())) { + if (!devToolsLoadedInBrowser) { + loadDevToolsInEmbeddedBrowser(project, toolWindow, flutterSdkVersion); + } + } + } + }); + Disposer.register(toolWindow.getDisposable(), connection); } } \ No newline at end of file From 5543975b1bd06c4704873433ed302e9207956e60 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Fri, 9 May 2025 11:30:15 -0700 Subject: [PATCH 05/11] Clean up DevToolsServerTask --- .../run/daemon/DevToolsServerTask.java | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/flutter-idea/src/io/flutter/run/daemon/DevToolsServerTask.java b/flutter-idea/src/io/flutter/run/daemon/DevToolsServerTask.java index 80d4fc8db8..1331795c59 100644 --- a/flutter-idea/src/io/flutter/run/daemon/DevToolsServerTask.java +++ b/flutter-idea/src/io/flutter/run/daemon/DevToolsServerTask.java @@ -56,7 +56,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; -import java.util.Optional; class DevToolsServerTask extends Task.Backgroundable { private static final Logger LOG = Logger.getInstance(DevToolsServerTask.class); @@ -123,18 +122,19 @@ public void run(@NotNull ProgressIndicator progressIndicator) { ImmutableList.of("devtools", "--machine"))); } - final Optional devToolsOptional = checkForDartPluginInitiatedDevToolsWithRetries(progressIndicator).get(); - if (devToolsOptional.isEmpty() || devToolsOptional.get() == null) { - System.out.println("DevTools is null!"); - cancelWithError(new TimeoutException("Timed-out waiting for the Dart Plugin to start DevTools.")); - return; - } - - devToolsFutureRef.get().complete(devToolsOptional.get()); + // Otherwise, wait for the Dart Plugin to start the DevTools server. + final CompletableFuture devToolsFuture = checkForDartPluginInitiatedDevToolsWithRetries(progressIndicator); + devToolsFuture.whenComplete((devTools, error) -> { + if (error != null) { + cancelWithError(new Exception(error)); + } + else { + devToolsFutureRef.get().complete(devTools); + } + }); } catch (java.util.concurrent.ExecutionException | InterruptedException e) { cancelWithError(e); - return; } } @@ -260,30 +260,31 @@ public void projectClosing(@NotNull Project project) { }); } - private CompletableFuture> checkForDartPluginInitiatedDevToolsWithRetries(ProgressIndicator progressIndicator) throws InterruptedException { + private CompletableFuture checkForDartPluginInitiatedDevToolsWithRetries(ProgressIndicator progressIndicator) + throws InterruptedException { progressIndicator.setText2("Waiting for Dart Plugin"); - final CompletableFuture> devToolsFuture = new CompletableFuture<>(); + final CompletableFuture devToolsFuture = new CompletableFuture<>(); final long msBetweenRetries = 1500; - int retries = 10; - while (retries >= 0) { + int retries = 0; + while (retries < 10) { + retries++; final double currentProgress = progressIndicator.getFraction(); progressIndicator.setFraction(Math.min(currentProgress + 10, 95)); final @Nullable DevToolsInstance devTools = createDevToolsInstanceFromDartPluginUri(); if (devTools != null) { - System.out.println("DevTools ready! retry number " + retries); - devToolsFuture.complete(Optional.of(devTools)); + LOG.debug("Dart Plugin DevTools ready after " + retries + " tries."); + devToolsFuture.complete(devTools); return devToolsFuture; } else { - System.out.println("DevTools not ready yet, retry number " + retries); + LOG.debug("Dart Plugin DevTools not ready after " + retries + " tries."); Thread.sleep(msBetweenRetries); - retries--; } } - devToolsFuture.complete(Optional.ofNullable(null)); + devToolsFuture.completeExceptionally(new Exception("Timed out waiting for Dart Plugin to start DevTools.")); return devToolsFuture; } From 5fed9e2870752c1210c2c25c3608d37157d0c88f Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Fri, 9 May 2025 12:33:06 -0700 Subject: [PATCH 06/11] Remove unused code --- flutter-idea/src/io/flutter/run/daemon/DevToolsServerTask.java | 1 - 1 file changed, 1 deletion(-) diff --git a/flutter-idea/src/io/flutter/run/daemon/DevToolsServerTask.java b/flutter-idea/src/io/flutter/run/daemon/DevToolsServerTask.java index 1331795c59..b78cc09d17 100644 --- a/flutter-idea/src/io/flutter/run/daemon/DevToolsServerTask.java +++ b/flutter-idea/src/io/flutter/run/daemon/DevToolsServerTask.java @@ -61,7 +61,6 @@ class DevToolsServerTask extends Task.Backgroundable { private static final Logger LOG = Logger.getInstance(DevToolsServerTask.class); public static final String LOCAL_DEVTOOLS_DIR = "flutter.local.devtools.dir"; public static final String LOCAL_DEVTOOLS_ARGS = "flutter.local.devtools.args"; - public @Nullable String failureMessage = null; private @NotNull Project project; private final AtomicReference> devToolsFutureRef; From bdfa9bede43068af9ae627fdbd6682acfe42a3d3 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Fri, 9 May 2025 12:36:51 -0700 Subject: [PATCH 07/11] Clean up from merge --- .../devtools/AbstractDevToolsViewFactory.java | 2 +- .../src/io/flutter/view/ViewUtils.java | 24 +------------------ 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/flutter-idea/src/io/flutter/devtools/AbstractDevToolsViewFactory.java b/flutter-idea/src/io/flutter/devtools/AbstractDevToolsViewFactory.java index 0ca69a7d39..a0fd46f5bb 100644 --- a/flutter-idea/src/io/flutter/devtools/AbstractDevToolsViewFactory.java +++ b/flutter-idea/src/io/flutter/devtools/AbstractDevToolsViewFactory.java @@ -170,4 +170,4 @@ public void toolWindowShown(@NotNull ToolWindow activatedToolWindow) { }); Disposer.register(toolWindow.getDisposable(), connection); } -} \ No newline at end of file +} diff --git a/flutter-idea/src/io/flutter/view/ViewUtils.java b/flutter-idea/src/io/flutter/view/ViewUtils.java index f11211f966..0b1541a866 100644 --- a/flutter-idea/src/io/flutter/view/ViewUtils.java +++ b/flutter-idea/src/io/flutter/view/ViewUtils.java @@ -5,7 +5,6 @@ */ package io.flutter.view; -import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.VerticalFlowLayout; import com.intellij.openapi.wm.ToolWindow; import com.intellij.ui.components.JBLabel; @@ -14,7 +13,6 @@ import com.intellij.ui.content.ContentManager; import com.intellij.util.ui.JBUI; import com.intellij.util.ui.UIUtil; -import io.flutter.run.daemon.DevToolsInstance; import io.flutter.utils.LabelInput; import io.flutter.utils.OpenApiUtils; import org.jetbrains.annotations.NotNull; @@ -25,7 +23,7 @@ public class ViewUtils { public void presentLabel(ToolWindow toolWindow, String text) { - final JBLabel label = new JBLabel("" + text + "", SwingConstants.CENTER); + final JBLabel label = new JBLabel(text, SwingConstants.CENTER); label.setForeground(UIUtil.getLabelDisabledForeground()); replacePanelLabel(toolWindow, label); } @@ -80,26 +78,6 @@ public void presentClickableLabel(ToolWindow toolWindow, List labels replacePanelLabel(toolWindow, center); } - public boolean verifyDevToolsPanelStateIsValid(ToolWindow toolWindow, Project project, DevToolsInstance instance, Throwable error) { - if (!project.isOpen()) { - presentLabel(toolWindow, "

Project is not open.

"); - return false; - } - - final String restartDevToolsMessage = "

Try switching to another Flutter panel and back again to re-start the server.

"; - if (error != null) { - presentLabel(toolWindow, "

Flutter DevTools start-up failed.

" + restartDevToolsMessage); - return false; - } - - if (instance == null) { - presentLabel(toolWindow, "

Flutter DevTools does not exist.

" + restartDevToolsMessage); - return false; - } - - return true; - } - public void replacePanelLabel(ToolWindow toolWindow, JComponent label) { OpenApiUtils.safeInvokeLater(() -> { final ContentManager contentManager = toolWindow.getContentManager(); From 54680f427153ea54ca7a337f7b965800f8a2eed5 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Fri, 9 May 2025 12:42:01 -0700 Subject: [PATCH 08/11] Remove unnecessary import --- .../src/io/flutter/deeplinks/DeepLinksViewFactory.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/flutter-idea/src/io/flutter/deeplinks/DeepLinksViewFactory.java b/flutter-idea/src/io/flutter/deeplinks/DeepLinksViewFactory.java index 83f170e65e..d6f84210f1 100644 --- a/flutter-idea/src/io/flutter/deeplinks/DeepLinksViewFactory.java +++ b/flutter-idea/src/io/flutter/deeplinks/DeepLinksViewFactory.java @@ -19,9 +19,6 @@ public class DeepLinksViewFactory extends AbstractDevToolsViewFactory { @NotNull public static String DEVTOOLS_PAGE_ID = "deep-links"; - @NotNull - private final ViewUtils viewUtils = new ViewUtils(); - @Override public boolean versionSupportsThisTool(@NotNull final FlutterSdkVersion flutterSdkVersion) { return flutterSdkVersion.canUseDeepLinksTool(); From a1f8a86f6448b10a9ce45b08b05a57a0ec7dc75d Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Fri, 9 May 2025 13:30:40 -0700 Subject: [PATCH 09/11] Make sure labels are not cut off --- flutter-idea/src/io/flutter/view/ViewUtils.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/flutter-idea/src/io/flutter/view/ViewUtils.java b/flutter-idea/src/io/flutter/view/ViewUtils.java index 0b1541a866..b5bd434bcc 100644 --- a/flutter-idea/src/io/flutter/view/ViewUtils.java +++ b/flutter-idea/src/io/flutter/view/ViewUtils.java @@ -23,7 +23,7 @@ public class ViewUtils { public void presentLabel(ToolWindow toolWindow, String text) { - final JBLabel label = new JBLabel(text, SwingConstants.CENTER); + final JBLabel label = new JBLabel(wrapWithHtml(text), SwingConstants.CENTER); label.setForeground(UIUtil.getLabelDisabledForeground()); replacePanelLabel(toolWindow, label); } @@ -39,7 +39,7 @@ public void presentLabels(@NotNull ToolWindow toolWindow, @NotNull List labelsPanel.setBorder(JBUI.Borders.empty()); // Use padding on individual labels if needed for (String text : labels) { - final JBLabel label = new JBLabel(text, SwingConstants.CENTER); + final JBLabel label = new JBLabel(wrapWithHtml(text), SwingConstants.CENTER); label.setForeground(UIUtil.getLabelDisabledForeground()); // Add padding to each label for spacing label.setBorder(JBUI.Borders.empty(2, 0)); @@ -59,13 +59,13 @@ public void presentClickableLabel(ToolWindow toolWindow, List labels for (LabelInput input : labels) { if (input.listener == null) { - final JLabel descriptionLabel = new JLabel("" + input.text + ""); + final JLabel descriptionLabel = new JLabel(wrapWithHtml(input.text)); descriptionLabel.setBorder(JBUI.Borders.empty(5)); descriptionLabel.setHorizontalAlignment(SwingConstants.CENTER); panel.add(descriptionLabel, BorderLayout.NORTH); } else { - final LinkLabel linkLabel = new LinkLabel<>("" + input.text + "", null); + final LinkLabel linkLabel = new LinkLabel<>(wrapWithHtml(input.text), null); linkLabel.setBorder(JBUI.Borders.empty(5)); linkLabel.setListener(input.listener, null); linkLabel.setHorizontalAlignment(SwingConstants.CENTER); @@ -92,4 +92,9 @@ public void replacePanelLabel(ToolWindow toolWindow, JComponent label) { contentManager.addContent(content); }); } + + private String wrapWithHtml(String text) { + // Wrapping with HTML tags ensures the text wraps and is not cut off. + return "" + text + ""; + } } From b9d2962d4de39619435f0ce972ce7fbff4b0e97e Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Fri, 9 May 2025 14:50:54 -0700 Subject: [PATCH 10/11] Respond to PR comments, make sure loading message shows up --- .../devtools/AbstractDevToolsViewFactory.java | 7 ++-- .../run/daemon/DevToolsServerTask.java | 37 ++++++++++--------- .../flutter/run/daemon/DevToolsService.java | 2 +- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/flutter-idea/src/io/flutter/devtools/AbstractDevToolsViewFactory.java b/flutter-idea/src/io/flutter/devtools/AbstractDevToolsViewFactory.java index a0fd46f5bb..d68c10fa9c 100644 --- a/flutter-idea/src/io/flutter/devtools/AbstractDevToolsViewFactory.java +++ b/flutter-idea/src/io/flutter/devtools/AbstractDevToolsViewFactory.java @@ -45,7 +45,8 @@ public abstract DevToolsUrl getDevToolsUrl(@NotNull Project project, @NotNull FlutterSdkVersion flutterSdkVersion, @NotNull DevToolsInstance instance); - protected void doAfterBrowserOpened(@NotNull Project project, @NotNull EmbeddedBrowser browser) {} + protected void doAfterBrowserOpened(@NotNull Project project, @NotNull EmbeddedBrowser browser) { + } private boolean devToolsLoadedInBrowser = false; @@ -112,11 +113,11 @@ public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindo private void loadDevToolsInEmbeddedBrowser(@NotNull Project project, @NotNull ToolWindow toolWindow, @NotNull FlutterSdkVersion flutterSdkVersion) { + viewUtils.presentLabel(toolWindow, "Loading " + getToolWindowTitle() + "..."); + AsyncUtils.whenCompleteUiThread( DevToolsService.getInstance(project).getDevToolsInstance(), (instance, error) -> { - viewUtils.presentLabel(toolWindow, "Loading " + getToolWindowTitle() + "..."); - // Skip displaying if the project has been closed. if (!project.isOpen()) { viewUtils.presentLabel(toolWindow, "Project is closed."); diff --git a/flutter-idea/src/io/flutter/run/daemon/DevToolsServerTask.java b/flutter-idea/src/io/flutter/run/daemon/DevToolsServerTask.java index b78cc09d17..0ff8d586eb 100644 --- a/flutter-idea/src/io/flutter/run/daemon/DevToolsServerTask.java +++ b/flutter-idea/src/io/flutter/run/daemon/DevToolsServerTask.java @@ -58,18 +58,18 @@ import java.util.concurrent.atomic.AtomicReference; class DevToolsServerTask extends Task.Backgroundable { - private static final Logger LOG = Logger.getInstance(DevToolsServerTask.class); - public static final String LOCAL_DEVTOOLS_DIR = "flutter.local.devtools.dir"; - public static final String LOCAL_DEVTOOLS_ARGS = "flutter.local.devtools.args"; - private @NotNull Project project; - private final AtomicReference> devToolsFutureRef; + private @NotNull static final Logger LOG = Logger.getInstance(DevToolsServerTask.class); + public @NotNull static final String LOCAL_DEVTOOLS_DIR = "flutter.local.devtools.dir"; + public @NotNull static final String LOCAL_DEVTOOLS_ARGS = "flutter.local.devtools.args"; + private @NotNull final Project project; + private @NotNull final AtomicReference> devToolsFutureRef; - private DaemonApi daemonApi; - private ProcessHandler process; + private @Nullable DaemonApi daemonApi; + private @Nullable ProcessHandler process; public DevToolsServerTask(@NotNull Project project, @NotNull String title, - AtomicReference> devToolsFutureRef) { + @NotNull AtomicReference> devToolsFutureRef) { super(project, title, true); this.project = project; this.devToolsFutureRef = devToolsFutureRef; @@ -84,6 +84,7 @@ public void run(@NotNull ProgressIndicator progressIndicator) { // If DevTools is not supported, start the daemon instead. final boolean dartDevToolsSupported = dartSdkSupportsDartDevTools(); if (!dartDevToolsSupported) { + LOG.info("Starting the DevTools daemon."); progressIndicator.setFraction(60); progressIndicator.setText2("Daemon set-up"); setUpWithDaemon(); @@ -106,13 +107,14 @@ public void run(@NotNull ProgressIndicator progressIndicator) { // https://github.com/flutter/flutter-intellij/blob/master/CONTRIBUTING.md#developing-with-local-devtools final String localDevToolsDir = Registry.stringValue(LOCAL_DEVTOOLS_DIR); if (!localDevToolsDir.isEmpty()) { + LOG.info("Starting local DevTools server at: " + localDevToolsDir); progressIndicator.setFraction(60); progressIndicator.setText2("Starting local server"); setUpLocalServer(localDevToolsDir); } // If the Dart plugin does not start DevTools, then call `dart devtools` to start the server. - final Boolean dartPluginStartsDevTools = true; + final boolean dartPluginStartsDevTools = true; if (!dartPluginStartsDevTools) { progressIndicator.setFraction(60); progressIndicator.setText2("Starting server"); @@ -149,11 +151,10 @@ private Boolean dartSdkSupportsDartDevTools() { @Override public void onThrowable(@NotNull Throwable error) { - super.onThrowable(error); cancelWithError(new Exception(error)); } - private void setUpLocalServer(String localDevToolsDir) throws java.util.concurrent.ExecutionException, InterruptedException { + private void setUpLocalServer(@NotNull String localDevToolsDir) throws java.util.concurrent.ExecutionException, InterruptedException { final DtdUtils dtdUtils = new DtdUtils(); final DartToolingDaemonService dtdService = dtdUtils.readyDtdService(project).get(); final String dtdUri = dtdService.getUri(); @@ -170,7 +171,7 @@ private void setUpLocalServer(String localDevToolsDir) throws java.util.concurre setUpInDevMode(createCommand(localDevToolsDir, "dt", args)); } - private void setUpInDevMode(GeneralCommandLine command) { + private void setUpInDevMode(@NotNull GeneralCommandLine command) { try { this.process = new MostlySilentColoredProcessHandler(command); this.process.addProcessListener(new ProcessAdapter() { @@ -259,9 +260,9 @@ public void projectClosing(@NotNull Project project) { }); } - private CompletableFuture checkForDartPluginInitiatedDevToolsWithRetries(ProgressIndicator progressIndicator) + private CompletableFuture checkForDartPluginInitiatedDevToolsWithRetries(@NotNull ProgressIndicator progressIndicator) throws InterruptedException { - progressIndicator.setText2("Waiting for Dart Plugin"); + progressIndicator.setText2("Waiting for Dart plugin"); final CompletableFuture devToolsFuture = new CompletableFuture<>(); final long msBetweenRetries = 1500; @@ -273,17 +274,17 @@ private CompletableFuture checkForDartPluginInitiatedDevToolsW final @Nullable DevToolsInstance devTools = createDevToolsInstanceFromDartPluginUri(); if (devTools != null) { - LOG.debug("Dart Plugin DevTools ready after " + retries + " tries."); + LOG.debug("Dart plugin DevTools ready after " + retries + " tries."); devToolsFuture.complete(devTools); return devToolsFuture; } else { - LOG.debug("Dart Plugin DevTools not ready after " + retries + " tries."); + LOG.debug("Dart plugin DevTools not ready after " + retries + " tries."); Thread.sleep(msBetweenRetries); } } - devToolsFuture.completeExceptionally(new Exception("Timed out waiting for Dart Plugin to start DevTools.")); + devToolsFuture.completeExceptionally(new Exception("Timed out waiting for Dart plugin to start DevTools.")); return devToolsFuture; } @@ -355,7 +356,7 @@ private static GeneralCommandLine chooseCommand(@NotNull final Project project) } } - private static GeneralCommandLine createCommand(String workDir, String command, List arguments) { + private static @NotNull GeneralCommandLine createCommand(String workDir, String command, List arguments) { final GeneralCommandLine result = new GeneralCommandLine().withWorkDirectory(workDir); result.setCharset(StandardCharsets.UTF_8); result.setExePath(FileUtil.toSystemDependentName(command)); diff --git a/flutter-idea/src/io/flutter/run/daemon/DevToolsService.java b/flutter-idea/src/io/flutter/run/daemon/DevToolsService.java index c5dee5fdbb..f22dc87c84 100644 --- a/flutter-idea/src/io/flutter/run/daemon/DevToolsService.java +++ b/flutter-idea/src/io/flutter/run/daemon/DevToolsService.java @@ -31,7 +31,7 @@ protected static class DevToolsServiceListener implements DaemonEvent.Listener { @Nullable private BackgroundableProcessIndicator devToolsServerProgressIndicator; - private AtomicReference> devToolsFutureRef = new AtomicReference<>(null); + @NotNull private AtomicReference> devToolsFutureRef = new AtomicReference<>(null); @NotNull public static DevToolsService getInstance(@NotNull final Project project) { From 641fd0c2758e2610a21a137afe313381fae4afe9 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Fri, 9 May 2025 14:55:55 -0700 Subject: [PATCH 11/11] PR Comments --- .../io/flutter/run/daemon/DevToolsServerTask.java | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/flutter-idea/src/io/flutter/run/daemon/DevToolsServerTask.java b/flutter-idea/src/io/flutter/run/daemon/DevToolsServerTask.java index 0ff8d586eb..fc1f723097 100644 --- a/flutter-idea/src/io/flutter/run/daemon/DevToolsServerTask.java +++ b/flutter-idea/src/io/flutter/run/daemon/DevToolsServerTask.java @@ -113,17 +113,7 @@ public void run(@NotNull ProgressIndicator progressIndicator) { setUpLocalServer(localDevToolsDir); } - // If the Dart plugin does not start DevTools, then call `dart devtools` to start the server. - final boolean dartPluginStartsDevTools = true; - if (!dartPluginStartsDevTools) { - progressIndicator.setFraction(60); - progressIndicator.setText2("Starting server"); - setUpWithDart(createCommand(DartSdk.getDartSdk(project).getHomePath(), - DartSdk.getDartSdk(project).getHomePath() + File.separatorChar + "bin" + File.separatorChar + "dart", - ImmutableList.of("devtools", "--machine"))); - } - - // Otherwise, wait for the Dart Plugin to start the DevTools server. + // Wait for the Dart Plugin to start the DevTools server. final CompletableFuture devToolsFuture = checkForDartPluginInitiatedDevToolsWithRetries(progressIndicator); devToolsFuture.whenComplete((devTools, error) -> { if (error != null) { @@ -262,7 +252,7 @@ public void projectClosing(@NotNull Project project) { private CompletableFuture checkForDartPluginInitiatedDevToolsWithRetries(@NotNull ProgressIndicator progressIndicator) throws InterruptedException { - progressIndicator.setText2("Waiting for Dart plugin"); + progressIndicator.setText2("Waiting for server with DTD"); final CompletableFuture devToolsFuture = new CompletableFuture<>(); final long msBetweenRetries = 1500;