Skip to content

Projects and Java fixes: 2026.2 platform, JDK setup, Gradle migration, Check-button deadlock - #62

Merged
Tsyklop merged 4 commits into
mainfrom
bug/project-and-java
Sep 25, 2026
Merged

Tsyklop merged 4 commits into
mainfrom
bug/project-and-java

Conversation

@Tsyklop

@Tsyklop Tsyklop commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator

Continues the bug/project-and-java work that #60 started, and adds a fix for the freeze reported in #61.

Four commits, 85 files. The last commit is the deadlock fix and is described in full below; the three projects and java fixes commits predate it and are summarised by theme.


Fix the permanent EDT deadlock on the Check button (#61)

ActionManager.tryToExecute(action, inputEvent, component, place, now = true) does not simply run an action. With now = true it takes this path:

tryToExecute -> ActionExecutionSupportKt.tryToExecuteNow
  -> Utils.runUpdateSessionForInputEvent
    -> Utils.runWithPotemkinOverlayProgress
      -> UtilsKt.runBlockingForActionExpand   (= runBlockingWithParallelismCompensation)

So it runs a full action update session and blocks the EDT waiting for it. For an ActionUpdateThread.BGT action that update needs a read action while the EDT is parked, which closes a cycle against a background VFS refresh:

edge waits for owner
EDT → action-update coroutine runBlockingForActionExpand plugin (tryToExecute(…, now = true))
coroutine → read action InternalReadAction.readLoop platform
read action → write lock held by bg VFS refresh, itself waiting on the EDT platform

There is no timeout, so the IDE has to be killed. The reporter confirmed this with two jstack samples 100 s apart showing bit-identical CPU counters on the write-lock holder. Clicking Check shortly after the course files are saved on disk is enough to lose the race.

Two shapes of fix, chosen per call site

CheckPanelButtonComponent — drop the update session. The panel already decides enablement (it passes isEnabled in and only attaches the listener when true), and all three actions it hosts — CheckAction, NextTaskAction, RetryAction — no-op their update() for the CheckPanel place. The update session computed nothing we used. It now builds the event directly and calls ActionUtil.performAction, which is exactly what the platform's own AnActionLink does — the same panel's "Peek solution" link already took that path without freezing.

The two course-selection sites — keep the update, move it off the EDT. SwitchTaskPanelAction.update genuinely disables itself (project != null && project.isEduProject() || ACTION_SEARCH == place), so skipping the update there would have silently changed behaviour. Those sites keep tryToExecute and pass now = false, which routes through tryToExecuteSuspend: it updates via Utils.expandActionGroupSuspend (suspending, never blocks the EDT), still honours presentation.isEnabled, and still performs the action on the EDT via runOnEdtWithConditionalWriteIntentSuspending.

invokeSwitchUILibrary ran doValidation on the next line, relying on tryToExecute being synchronous, so that is now chained off the returned ActionCallback. It uses doWhenProcessed rather than doWhenDone because doValidation currently runs even when the action is rejected, and it marshals through invokeLater with a captured ModalityState because the reject path resolves the callback on a background thread while doValidation touches Swing, and the course dialog is modal.

Notes for review

  • Cross-version: ActionUtil.performAction, AnActionEvent.createEvent (6-arg), ActionUiKind, ActionCallback.doWhenProcessed and ModalityState.stateForComponent all have byte-identical signatures on 252 / 253 / 261 / 262 and none is @ApiStatus.Internal, so no branches/<version> split was needed.
  • Why performAction and not invokeAction: the invokeAction overloads are a deprecation chain ending at performAction. These are Kotlin @Deprecated, invisible to javap — only compiling reveals them.
  • actionPerformed on the EDT is still safe: FileDocumentManager.saveAllDocuments() early-returns before taking any lock when nothing is unsaved, and otherwise wraps the save in a PotemkinProgress whose EventStealer.isUrgentInvocationEvent explicitly whitelists InternalThreading$TransferredWriteActionEvent, so a pending transferred write action does get dispatched.
  • createHyperlinkWithContextHelp in newproject/ui/utils.kt is dead code — CoursesPanel.toolbarAction() returns null and is never overridden, so no ToolbarActionWrapper is ever constructed. It was converted for consistency; deleting it might be the better cleanup.
  • Not verified by execution. Nothing in the test suite reaches any of these three paths (no test references CheckPanelButtonComponent), and the underlying race was not reproduced locally. The claim supported here is that the lock cycle is structurally broken, not that the freeze is confirmed gone in the wild. :intellij-plugin:hs-core:compileKotlin is clean.

Earlier commits on this branch

Summarised from the diff rather than from having authored or reviewed them — worth a closer look from whoever wrote them.

  • Platform 2026.2 support — environmentName bump, libs.versions.toml and gradle-252/253.properties updates, intellij-plugin-common-conventions.gradle.kts and intellijUtils.kt changes, plus new TestOnlyThreadingCompat and RefreshQueueCompat shims under all four branches/<version> trees.
  • JVM course JDK setup — new JdkAutoInstaller.kt, ProjectJdkRepair.kt and a JdkDownloadUi.java shim; a large rewrite of JdkLanguageSettings.kt; changes to JdkProjectSettings, JdkEnvironmentSettings and ParsedJavaVersion; SqlJdkLanguageSettings folded away. Covered by the new JdkSelectionTest.
  • Gradle script migration — new GradleScriptMigration.kt, slimmed GradleStartupActivity, EduGradleUtils changes and a new settings-script template, with GradleAdditionalFilesMigrationTest and GradleSettingsScriptMigrationTest.
  • Course view nodes — EduNode, DirectoryNode, LessonNode, SectionNode, FrameworkLessonNode, CCSectionNode and CourseViewUtils, with NodesTest extended.
  • YAML and study-item updates — YamlLoader, YamlDeepLoader, YamlFormatSynchronizer, StudyItemChangeApplier, StudyItem, ItemContainer and the *Updater classes, with a new ItemContainerTest and an added HyperskillSectionUpdateTest case.
  • Hyperskill submissions — HyperskillCheckConnector, HyperskillConnector, StepikAPI and RetrofitExt, with a new HyperskillRejectedEduTaskSubmissionTest.
  • Task tool window — JCEFToolWindow, TabManager, DescriptionTab, CodeHighlighter, HintsWrapper.

🤖 Generated with Claude Code

Tsyklop and others added 4 commits August 24, 2026 17:52
ActionManager.tryToExecute(..., now = true) does not just run an action:
it runs a full action update session and blocks the EDT in
runBlockingForActionExpand waiting for it. For an ActionUpdateThread.BGT
action that update needs a read action, which deadlocks against a
background VFS refresh that holds the write lock and is itself waiting to
be serviced on the EDT. The cycle has no timeout, so the IDE has to be
killed. Clicking Check shortly after the course files are saved on disk
is enough to lose the race.

CheckPanelButtonComponent decides button enablement itself, and all three
actions it hosts (CheckAction, NextTaskAction, RetryAction) no-op their
update() for the CheckPanel place, so the update session computes nothing
we use. Skip it and call ActionUtil.performAction directly, which is what
the platform's own AnActionLink does.

The two course-selection call sites do rely on the action's own update(),
so they keep tryToExecute and instead pass now = false. That routes
through tryToExecuteSuspend, which updates via expandActionGroupSuspend
without blocking the EDT, still honours presentation.isEnabled, and still
performs the action on the EDT. doValidation is chained off the returned
ActionCallback via invokeLater with the dialog's ModalityState, since that
callback can be resolved on a background thread and doValidation touches
Swing.

Fixes #61

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Tsyklop
Tsyklop merged commit 8751857 into main Sep 25, 2026
0 of 2 checks passed
@Tsyklop
Tsyklop deleted the bug/project-and-java branch September 25, 2026 08:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant