Avoid project.save() on the EDT during Flutter module setup - #9102
mehmet-emre-sezer wants to merge 2 commits into
Conversation
project.save() on the EDT enters a nested modal pump while the document save it dispatches calls back into the EDT, so opening a project whose Flutter SDK path does not resolve hangs the IDE with no timeout. The call was left behind by flutter#8903, which removed the reloadProject() that it used to flush the module type for. Fixes flutter#9093
There was a problem hiding this comment.
Code Review
This pull request resolves an IDE deadlock during project startup by removing a synchronous project.save() call from an asynchronous invokeLater block in FlutterModuleUtils.java. The review feedback recommends adding a guard check to ensure that neither the project nor the module has been disposed before executing the asynchronous block, preventing potential AlreadyDisposedException errors.
| ApplicationManager.getApplication().invokeLater(() -> { | ||
| ApplicationManager.getApplication().runWriteAction(() -> setFlutterModuleType(module)); |
There was a problem hiding this comment.
[CONCERN] Since this block is executed asynchronously on the EDT via invokeLater, the project or module might be disposed by the time the runnable is executed (e.g., if the user closes the project or IDE quickly during startup). To prevent AlreadyDisposedException, we should add a guard check for disposal at the beginning of the lambda.
| ApplicationManager.getApplication().invokeLater(() -> { | |
| ApplicationManager.getApplication().runWriteAction(() -> setFlutterModuleType(module)); | |
| ApplicationManager.getApplication().invokeLater(() -> { | |
| if (project.isDisposed() || module.isDisposed()) return; | |
| ApplicationManager.getApplication().runWriteAction(() -> setFlutterModuleType(module)); |
There was a problem hiding this comment.
Thanks — the race is real, but I'd rather not fold it into this PR.
It predates this change and is unrelated to the deadlock in #9093. More
importantly, the suggested guard would only cover part of it: enableDartSDK()
schedules its own invokeLater a few lines down with no disposal check, so
guarding only this lambda would leave the same race open one call deeper while
looking like it was handled.
Happy to file a separate issue and fix both properly in a follow-up PR if
maintainers agree that's the right scope.
What and why
FlutterModuleUtils.setFlutterModuleWithoutReload()calledproject.save()froman
invokeLaterblock on the EDT.ProjectImpl.save()enters a nested modalprogress pump on the EDT, while the document save it dispatches calls back into
the EDT via
invokeAndWaitforbeforeAllDocumentsSaving. Neither side canproceed and the wait has no timeout, so opening a project whose Flutter SDK path
does not resolve hangs the IDE permanently — the user cannot even reach Settings
to correct the path.
The call was also redundant. It was left behind by #8903, which removed the
ProjectManager.reloadProject(project)that used to follow it. Its only purposewas to flush the module type to disk before that reload read it back; with the
reload gone, the platform persists the module type on its own schedule.
This PR removes the call and leaves a comment explaining why it must not come
back, following the existing convention in this file (see the comment
referencing #8480 a few lines below).
One small correction to the issue report: the deadlock does not require the
write lock.
runWriteActionon the preceding line has already exited by thetime
save()runs —runIntendedWriteActionOnCurrentThreadin the stack traceis
invokeLater's write-intent wrapper, not a held write lock. Being on the EDTis sufficient to reproduce it. This does not change the fix.
Related issues
Fixes #9093
Same EDT re-entrancy class as #9013 and #9055, different call site.
How to verify
dangling symlink, e.g. from an FVM
cachePathchange).module configuration". After it, project open completes and the IDE stays
responsive.
The full unit test suite passes locally (32 classes, 160 tests, 0 failures),
including
FlutterModuleUtilsTestandFlutterInitializerTest.On testing
I did not add a regression test. The deadlock needs a real modal progress pump
and the document-save listener chain, neither of which the headless test fixture
sets up, so a test around
setFlutterModuleWithoutReload()would pass againstthe old code too and guard nothing. The method also reaches
FlutterSdkUtil.locateSdkFromPath(), which makes any test of it dependent onthe host's PATH. I opted for the explanatory comment instead, but I'm happy to
add a test if you see an angle I missed.
Review the contribution guidelines below:
AUTHORSfile.CHANGELOG.mdif appropriate.Contribution guidelines:
our contributor guide and
the Flutter organization contributor guide
for general expectations for PRs.
dart format.practices (discussion).