[debugger hotswap] Move HotSwapDebugSessionManager to platform

GitOrigin-RevId: 736c5f99fbcbc7c5bfcec59157f69db1e5fc184d
This commit is contained in:
Maksim Zuev
2025-06-06 12:21:29 +00:00
committed by intellij-monorepo-bot
parent 9f1bb76ee7
commit 49d5049d30
7 changed files with 143 additions and 106 deletions
@@ -95,7 +95,7 @@
<applicationService serviceImplementation="com.intellij.debugger.settings.ThreadsViewSettings"/>
<applicationService serviceImplementation="com.intellij.debugger.settings.TraceSettings"/>
<projectService serviceInterface="com.intellij.debugger.ui.HotSwapUI" serviceImplementation="com.intellij.debugger.ui.HotSwapUIImpl"/>
<postStartupActivity implementation="com.intellij.debugger.impl.hotswap.HotSwapManagerInitActivity"/>
<xdebugger.hotSwapInDebugSessionEnabler implementation="com.intellij.debugger.impl.hotswap.JvmHotSwapInDebugSessionEnabler"/>
<projectService serviceInterface="com.intellij.debugger.DebuggerManager"
serviceImplementation="com.intellij.debugger.impl.DebuggerManagerImpl"/>
<debugger.additionalContextProvider implementation="com.intellij.debugger.engine.evaluation.MarkedObjectAdditionalContextProvider"/>
@@ -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<HotSwapDebugSessionManager>()
}
}
@Service(Service.Level.PROJECT)
internal class HotSwapDebugSessionManager(project: Project, cs: CoroutineScope) : XDebuggerManagerListener {
private val sessions = ConcurrentHashMap<DebuggerSession, JavaHotSwapSessionEntry>()
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<DebuggerSession>?) {
sessions?.createListeners(originalSession)?.forEach { it.onSuccessfulReload() }
listener.onSuccessfulReload()
}
override fun onFailure(sessions: MutableList<DebuggerSession>?) {
sessions?.createListeners(originalSession)?.forEach { it.onFailure() }
listener.onFailure()
}
override fun onCancel(sessions: MutableList<DebuggerSession>?) {
sessions?.createListeners(originalSession)?.forEach { it.onCanceled() }
listener.onCanceled()
}
override fun onNothingToReload(sessions: MutableList<DebuggerSession>?) {
sessions?.createListeners(originalSession)?.forEach { it.onFinish() }
listener.onFinish()
}
private fun List<DebuggerSession>.createListeners(except: DebuggerSession) = filter { it !== except }
.mapNotNull { HotSwapDebugSessionManager.getInstance(except.project).findHotSwapSession(it) }
.map { it.startHotSwapListening() }
}
@@ -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<DebuggerSession>?) {
sessions?.createListeners(originalSession)?.forEach { it.onSuccessfulReload() }
listener.onSuccessfulReload()
}
override fun onFailure(sessions: MutableList<DebuggerSession>?) {
sessions?.createListeners(originalSession)?.forEach { it.onFailure() }
listener.onFailure()
}
override fun onCancel(sessions: MutableList<DebuggerSession>?) {
sessions?.createListeners(originalSession)?.forEach { it.onCanceled() }
listener.onCanceled()
}
override fun onNothingToReload(sessions: MutableList<DebuggerSession>?) {
sessions?.createListeners(originalSession)?.forEach { it.onFinish() }
listener.onFinish()
}
private fun List<DebuggerSession>.createListeners(except: DebuggerSession) = filter { it !== except }
.mapNotNull { HotSwapDebugSessionManager.getInstance(except.project).findHotSwapSession(it.xDebugSession!!.debugProcess) }
.map { it.startHotSwapListening() }
}
@@ -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<DebuggerSession> 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) {
@@ -28,6 +28,7 @@
<extensionPoint name="xdebugger.controlExceptionBreakpointSupport" interface="com.intellij.xdebugger.codeinsight.ControlExceptionBreakpointSupport" dynamic="true"/>
<extensionPoint name="xdebugger.hotSwapUiExtension" interface="com.intellij.xdebugger.impl.hotswap.HotSwapUiExtension" dynamic="true"/>
<extensionPoint name="xdebugger.hotSwapInDebugSessionEnabler" interface="com.intellij.xdebugger.impl.hotswap.HotSwapInDebugSessionEnabler" dynamic="true"/>
<extensionPoint name="xdebugger.customEvaluateHandler"
interface="com.intellij.xdebugger.impl.actions.handlers.XDebuggerCustomEvaluateHandler" dynamic="true"/>
@@ -121,6 +122,7 @@
<projectService serviceImplementation="com.intellij.xdebugger.memory.component.InstancesTracker" />
<projectService serviceImplementation="com.intellij.xdebugger.impl.frame.XDebuggerExecutionStackDescriptionService" />
<postStartupActivity implementation="com.intellij.xdebugger.impl.hotswap.HotSwapManagerInitActivity"/>
<titleInfoProvider implementation="com.intellij.xdebugger.impl.ui.DebuggerTitleInfoProvider"/>
<notificationGroup id="Debugger messages" displayType="TOOL_WINDOW" toolWindowId="Debug" isLogByDefault="false" bundle="messages.XDebuggerBundle" key="notification.group.debugger.messages"/>
@@ -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<XDebugProcess, HotSwapSessionEntry>()
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<HotSwapDebugSessionManager>()
}
}
@@ -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<T> {
@@ -34,6 +38,22 @@ interface HotSwapProvider<T> {
fun performHotSwap(session: HotSwapSession<T>)
}
/**
* Implement this to enable hotswap in debug sessions.
*/
@ApiStatus.Internal
interface HotSwapInDebugSessionEnabler {
fun createProvider(process: XDebugProcess): HotSwapProvider<*>?
companion object {
private val EP_NAME = ExtensionPointName<HotSwapInDebugSessionEnabler>("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