From 49d5049d30221eee5351693ce5d7a50965eede3e Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Fri, 6 Jun 2025 12:45:41 +0200 Subject: [PATCH] [debugger hotswap] Move HotSwapDebugSessionManager to platform GitOrigin-RevId: 736c5f99fbcbc7c5bfcec59157f69db1e5fc184d --- .../impl/resources/META-INF/java-debugger.xml | 2 +- .../hotswap/HotSwapDebugSessionListener.kt | 100 ------------------ .../JvmHotSwapInDebugSessionEnabler.kt | 50 +++++++++ .../intellij/debugger/ui/HotSwapUIImpl.java | 9 +- .../resources/META-INF/xdebugger.xml | 2 + .../hotswap/HotSwapDebugSessionManager.kt | 66 ++++++++++++ .../intellij/xdebugger/impl/hotswap/api.kt | 20 ++++ 7 files changed, 143 insertions(+), 106 deletions(-) delete mode 100644 java/debugger/impl/src/com/intellij/debugger/impl/hotswap/HotSwapDebugSessionListener.kt create mode 100644 java/debugger/impl/src/com/intellij/debugger/impl/hotswap/JvmHotSwapInDebugSessionEnabler.kt create mode 100644 platform/xdebugger-impl/src/com/intellij/xdebugger/impl/hotswap/HotSwapDebugSessionManager.kt diff --git a/java/debugger/impl/resources/META-INF/java-debugger.xml b/java/debugger/impl/resources/META-INF/java-debugger.xml index 98483367c378..5a46251d3f6b 100644 --- a/java/debugger/impl/resources/META-INF/java-debugger.xml +++ b/java/debugger/impl/resources/META-INF/java-debugger.xml @@ -95,7 +95,7 @@ - + diff --git a/java/debugger/impl/src/com/intellij/debugger/impl/hotswap/HotSwapDebugSessionListener.kt b/java/debugger/impl/src/com/intellij/debugger/impl/hotswap/HotSwapDebugSessionListener.kt deleted file mode 100644 index f62c0ae9d1b1..000000000000 --- a/java/debugger/impl/src/com/intellij/debugger/impl/hotswap/HotSwapDebugSessionListener.kt +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -package com.intellij.debugger.impl.hotswap - -import com.intellij.debugger.engine.JavaDebugProcess -import com.intellij.debugger.impl.DebuggerSession -import com.intellij.debugger.ui.HotSwapStatusListener -import com.intellij.openapi.Disposable -import com.intellij.openapi.components.Service -import com.intellij.openapi.components.service -import com.intellij.openapi.components.serviceAsync -import com.intellij.openapi.project.Project -import com.intellij.openapi.startup.ProjectActivity -import com.intellij.openapi.util.Disposer -import com.intellij.openapi.util.registry.Registry -import com.intellij.util.asDisposable -import com.intellij.xdebugger.XDebugProcess -import com.intellij.xdebugger.XDebugSession -import com.intellij.xdebugger.XDebuggerManager -import com.intellij.xdebugger.XDebuggerManagerListener -import com.intellij.xdebugger.impl.XDebugSessionImpl -import com.intellij.xdebugger.impl.hotswap.HotSwapResultListener -import com.intellij.xdebugger.impl.hotswap.HotSwapSession -import com.intellij.xdebugger.impl.hotswap.HotSwapSessionManager -import kotlinx.coroutines.CoroutineScope -import java.util.concurrent.ConcurrentHashMap - -private data class JavaHotSwapSessionEntry(val hotSwapSession: HotSwapSession<*>, val disposable: Disposable) - -private class HotSwapManagerInitActivity : ProjectActivity { - override suspend fun execute(project: Project) { - project.serviceAsync() - } -} - -@Service(Service.Level.PROJECT) -internal class HotSwapDebugSessionManager(project: Project, cs: CoroutineScope) : XDebuggerManagerListener { - private val sessions = ConcurrentHashMap() - - init { - project.getMessageBus().connect(cs).subscribe(XDebuggerManager.TOPIC, this) - } - - fun createSessionListenerOrNull(session: DebuggerSession): HotSwapStatusListener? { - val hotSwapSession = findHotSwapSession(session) ?: return null - return HotSwapStatusListenerAdapter(session, hotSwapSession.startHotSwapListening()) - } - - internal fun findHotSwapSession(session: DebuggerSession): HotSwapSession<*>? = sessions[session]?.hotSwapSession - - override fun processStarted(debugProcess: XDebugProcess) { - val session = (debugProcess as? JavaDebugProcess)?.debuggerSession ?: return - if (!Registry.`is`("debugger.hotswap.floating.toolbar")) return - val disposable = (debugProcess.session as XDebugSessionImpl).coroutineScope.asDisposable() - val hotSwapSession = HotSwapSessionManager.getInstance(session.project).createSession(JvmHotSwapProvider(session), disposable) - sessions[session] = JavaHotSwapSessionEntry(hotSwapSession, disposable) - } - - override fun processStopped(debugProcess: XDebugProcess) { - val javaDebugSession = (debugProcess as? JavaDebugProcess)?.debuggerSession ?: return - val sessionEntry = sessions.remove(javaDebugSession) ?: return - Disposer.dispose(sessionEntry.disposable) - } - - override fun currentSessionChanged(previousSession: XDebugSession?, currentSession: XDebugSession?) { - if (currentSession == null) return - val (_, entry) = sessions.entries.firstOrNull { (session, _) -> session.xDebugSession === currentSession } ?: return - HotSwapSessionManager.getInstance(currentSession.project).onSessionSelected(entry.hotSwapSession) - } - - companion object { - @JvmStatic - fun getInstance(project: Project): HotSwapDebugSessionManager = project.service() - } -} - -private class HotSwapStatusListenerAdapter(private val originalSession: DebuggerSession, private val listener: HotSwapResultListener) : HotSwapStatusListener { - override fun onSuccess(sessions: MutableList?) { - sessions?.createListeners(originalSession)?.forEach { it.onSuccessfulReload() } - listener.onSuccessfulReload() - } - - override fun onFailure(sessions: MutableList?) { - sessions?.createListeners(originalSession)?.forEach { it.onFailure() } - listener.onFailure() - } - - override fun onCancel(sessions: MutableList?) { - sessions?.createListeners(originalSession)?.forEach { it.onCanceled() } - listener.onCanceled() - } - - override fun onNothingToReload(sessions: MutableList?) { - sessions?.createListeners(originalSession)?.forEach { it.onFinish() } - listener.onFinish() - } - - private fun List.createListeners(except: DebuggerSession) = filter { it !== except } - .mapNotNull { HotSwapDebugSessionManager.getInstance(except.project).findHotSwapSession(it) } - .map { it.startHotSwapListening() } -} diff --git a/java/debugger/impl/src/com/intellij/debugger/impl/hotswap/JvmHotSwapInDebugSessionEnabler.kt b/java/debugger/impl/src/com/intellij/debugger/impl/hotswap/JvmHotSwapInDebugSessionEnabler.kt new file mode 100644 index 000000000000..bbe920c1fdb6 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/impl/hotswap/JvmHotSwapInDebugSessionEnabler.kt @@ -0,0 +1,50 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.debugger.impl.hotswap + +import com.intellij.debugger.engine.JavaDebugProcess +import com.intellij.debugger.impl.DebuggerSession +import com.intellij.debugger.ui.HotSwapStatusListener +import com.intellij.xdebugger.XDebugProcess +import com.intellij.xdebugger.impl.hotswap.HotSwapDebugSessionManager +import com.intellij.xdebugger.impl.hotswap.HotSwapInDebugSessionEnabler +import com.intellij.xdebugger.impl.hotswap.HotSwapProvider +import com.intellij.xdebugger.impl.hotswap.HotSwapResultListener + +private class JvmHotSwapInDebugSessionEnabler : HotSwapInDebugSessionEnabler { + override fun createProvider(process: XDebugProcess): HotSwapProvider<*>? { + val session = (process as? JavaDebugProcess)?.debuggerSession ?: return null + return JvmHotSwapProvider(session) + } +} + +internal fun createHotSwapSessionListenerOrNull(session: DebuggerSession): HotSwapStatusListener? { + val process = session.xDebugSession?.debugProcess ?: return null + val hotSwapSession = HotSwapDebugSessionManager.getInstance(session.project).findHotSwapSession(process) ?: return null + return HotSwapStatusListenerAdapter(session, hotSwapSession.startHotSwapListening()) +} + +private class HotSwapStatusListenerAdapter(private val originalSession: DebuggerSession, private val listener: HotSwapResultListener) : HotSwapStatusListener { + override fun onSuccess(sessions: MutableList?) { + sessions?.createListeners(originalSession)?.forEach { it.onSuccessfulReload() } + listener.onSuccessfulReload() + } + + override fun onFailure(sessions: MutableList?) { + sessions?.createListeners(originalSession)?.forEach { it.onFailure() } + listener.onFailure() + } + + override fun onCancel(sessions: MutableList?) { + sessions?.createListeners(originalSession)?.forEach { it.onCanceled() } + listener.onCanceled() + } + + override fun onNothingToReload(sessions: MutableList?) { + sessions?.createListeners(originalSession)?.forEach { it.onFinish() } + listener.onFinish() + } + + private fun List.createListeners(except: DebuggerSession) = filter { it !== except } + .mapNotNull { HotSwapDebugSessionManager.getInstance(except.project).findHotSwapSession(it.xDebugSession!!.debugProcess) } + .map { it.startHotSwapListening() } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/ui/HotSwapUIImpl.java b/java/debugger/impl/src/com/intellij/debugger/ui/HotSwapUIImpl.java index f01aa74b255b..41b83ad4c8da 100644 --- a/java/debugger/impl/src/com/intellij/debugger/ui/HotSwapUIImpl.java +++ b/java/debugger/impl/src/com/intellij/debugger/ui/HotSwapUIImpl.java @@ -7,7 +7,7 @@ import com.intellij.debugger.impl.DebuggerManagerListener; import com.intellij.debugger.impl.DebuggerSession; import com.intellij.debugger.impl.HotSwapFile; import com.intellij.debugger.impl.HotSwapManager; -import com.intellij.debugger.impl.hotswap.HotSwapDebugSessionManager; +import com.intellij.debugger.impl.hotswap.JvmHotSwapInDebugSessionEnablerKt; import com.intellij.debugger.settings.DebuggerSettings; import com.intellij.notification.Notification; import com.intellij.notification.NotificationType; @@ -295,7 +295,7 @@ public final class HotSwapUIImpl extends HotSwapUI { @Nullable HotSwapStatusListener callback) { dontAskHotswapAfterThisCompilation(); Project project = session.getProject(); - callback = mergeCallbacksIfNeeded(callback, HotSwapDebugSessionManager.getInstance(project).createSessionListenerOrNull(session)); + callback = mergeCallbacksIfNeeded(callback, JvmHotSwapInDebugSessionEnablerKt.createHotSwapSessionListenerOrNull(session)); if (compileBeforeHotswap) { ProjectTaskManagerImpl.putBuildOriginator(project, this.getClass()); ProjectTaskManager projectTaskManager = ProjectTaskManager.getInstance(project); @@ -323,7 +323,7 @@ public final class HotSwapUIImpl extends HotSwapUI { Project project = session.getProject(); ProjectTaskManagerImpl.putBuildOriginator(project, this.getClass()); - HotSwapStatusListener callback = HotSwapDebugSessionManager.getInstance(project).createSessionListenerOrNull(session); + HotSwapStatusListener callback = JvmHotSwapInDebugSessionEnablerKt.createHotSwapSessionListenerOrNull(session); if (callback == null) { ProjectTaskManager.getInstance(project).compile(files); } @@ -399,9 +399,8 @@ public final class HotSwapUIImpl extends HotSwapUI { boolean alwaysDoHotSwapOnRebuild = instance.myAskBeforeHotswap && DebuggerSettings.RUN_HOTSWAP_ALWAYS.equals(DebuggerSettings.getInstance().RUN_HOTSWAP_AFTER_COMPILE); List sessions = getHotSwappableDebugSessions(myProject); - HotSwapDebugSessionManager manager = HotSwapDebugSessionManager.getInstance(myProject); for (DebuggerSession session : sessions) { - HotSwapStatusListener listener = manager.createSessionListenerOrNull(session); + HotSwapStatusListener listener = JvmHotSwapInDebugSessionEnablerKt.createHotSwapSessionListenerOrNull(session); if (listener == null) continue; context.putUserData(HOT_SWAP_CALLBACK_KEY, listener); if (alwaysDoHotSwapOnRebuild) { diff --git a/platform/xdebugger-impl/resources/META-INF/xdebugger.xml b/platform/xdebugger-impl/resources/META-INF/xdebugger.xml index e16ec196e496..1e36e4a4f7a0 100644 --- a/platform/xdebugger-impl/resources/META-INF/xdebugger.xml +++ b/platform/xdebugger-impl/resources/META-INF/xdebugger.xml @@ -28,6 +28,7 @@ + @@ -121,6 +122,7 @@ + diff --git a/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/hotswap/HotSwapDebugSessionManager.kt b/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/hotswap/HotSwapDebugSessionManager.kt new file mode 100644 index 000000000000..5a27f7976dcf --- /dev/null +++ b/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/hotswap/HotSwapDebugSessionManager.kt @@ -0,0 +1,66 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.xdebugger.impl.hotswap + +import com.intellij.openapi.Disposable +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.components.serviceAsync +import com.intellij.openapi.project.Project +import com.intellij.openapi.startup.ProjectActivity +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.registry.Registry +import com.intellij.util.asDisposable +import com.intellij.xdebugger.XDebugProcess +import com.intellij.xdebugger.XDebugSession +import com.intellij.xdebugger.XDebuggerManager +import com.intellij.xdebugger.XDebuggerManagerListener +import com.intellij.xdebugger.impl.XDebugSessionImpl +import kotlinx.coroutines.CoroutineScope +import org.jetbrains.annotations.ApiStatus +import java.util.concurrent.ConcurrentHashMap + + +@ApiStatus.Internal +@Service(Service.Level.PROJECT) +class HotSwapDebugSessionManager(project: Project, cs: CoroutineScope) : XDebuggerManagerListener { + private val sessions = ConcurrentHashMap() + + init { + project.getMessageBus().connect(cs).subscribe(XDebuggerManager.TOPIC, this) + } + + fun findHotSwapSession(process: XDebugProcess): HotSwapSession<*>? = sessions[process]?.hotSwapSession + + override fun processStarted(debugProcess: XDebugProcess) { + if (!Registry.`is`("debugger.hotswap.floating.toolbar")) return + val provider = HotSwapInDebugSessionEnabler.createProviderForProcess(debugProcess) ?: return + val xDebugSession = debugProcess.session as XDebugSessionImpl + val disposable = xDebugSession.coroutineScope.asDisposable() + val hotSwapSession = HotSwapSessionManager.getInstance(xDebugSession.project).createSession(provider, disposable) + sessions[debugProcess] = HotSwapSessionEntry(hotSwapSession, disposable) + } + + override fun processStopped(debugProcess: XDebugProcess) { + val sessionEntry = sessions.remove(debugProcess) ?: return + Disposer.dispose(sessionEntry.disposable) + } + + override fun currentSessionChanged(previousSession: XDebugSession?, currentSession: XDebugSession?) { + if (currentSession == null) return + val (_, entry) = sessions.entries.firstOrNull { (process, _) -> process.session === currentSession } ?: return + HotSwapSessionManager.getInstance(currentSession.project).onSessionSelected(entry.hotSwapSession) + } + + companion object { + @JvmStatic + fun getInstance(project: Project): HotSwapDebugSessionManager = project.service() + } +} + +private data class HotSwapSessionEntry(val hotSwapSession: HotSwapSession<*>, val disposable: Disposable) + +private class HotSwapManagerInitActivity : ProjectActivity { + override suspend fun execute(project: Project) { + project.serviceAsync() + } +} \ No newline at end of file diff --git a/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/hotswap/api.kt b/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/hotswap/api.kt index ad280493c3bf..cc59480e86de 100644 --- a/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/hotswap/api.kt +++ b/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/hotswap/api.kt @@ -2,6 +2,8 @@ package com.intellij.xdebugger.impl.hotswap import com.intellij.openapi.Disposable +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.xdebugger.XDebugProcess import kotlinx.coroutines.CoroutineScope import org.jetbrains.annotations.ApiStatus @@ -9,6 +11,8 @@ import org.jetbrains.annotations.ApiStatus * Provides platform-specific implementation for the hot swap process. * * The provider should be passed to [HotSwapSessionManager.createSession] on the start of a session where hot swapping is possible. + * + * @see [HotSwapInDebugSessionEnabler] */ @ApiStatus.Internal interface HotSwapProvider { @@ -34,6 +38,22 @@ interface HotSwapProvider { fun performHotSwap(session: HotSwapSession) } +/** + * Implement this to enable hotswap in debug sessions. + */ +@ApiStatus.Internal +interface HotSwapInDebugSessionEnabler { + fun createProvider(process: XDebugProcess): HotSwapProvider<*>? + + companion object { + private val EP_NAME = ExtensionPointName("com.intellij.xdebugger.hotSwapInDebugSessionEnabler") + + internal fun createProviderForProcess(process: XDebugProcess): HotSwapProvider<*>? { + return EP_NAME.computeSafeIfAny { it.createProvider(process) } + } + } +} + /** * Listener to report the hot swap status. * @see HotSwapSession.startHotSwapListening