diff --git a/java/debugger/backend/src/com/intellij/java/debugger/impl/backend/BackendJavaDebuggerManagerApi.kt b/java/debugger/backend/src/com/intellij/java/debugger/impl/backend/BackendJavaDebuggerManagerApi.kt new file mode 100644 index 000000000000..ecf76869f043 --- /dev/null +++ b/java/debugger/backend/src/com/intellij/java/debugger/impl/backend/BackendJavaDebuggerManagerApi.kt @@ -0,0 +1,25 @@ +// 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.java.debugger.impl.backend + +import com.intellij.debugger.engine.JavaDebugProcess +import com.intellij.debugger.impl.DebuggerSession +import com.intellij.java.debugger.impl.shared.rpc.JavaDebuggerManagerApi +import com.intellij.java.debugger.impl.shared.rpc.JavaDebuggerSessionDto +import com.intellij.xdebugger.impl.rpc.XDebugSessionId +import com.intellij.xdebugger.impl.rpc.models.findValue +import fleet.rpc.core.toRpc +import kotlinx.coroutines.flow.map + +internal class BackendJavaDebuggerManagerApi : JavaDebuggerManagerApi { + override suspend fun getJavaSession(sessionId: XDebugSessionId): JavaDebuggerSessionDto? { + val xSession = sessionId.findValue() ?: return null + val process = xSession.debugProcess as? JavaDebugProcess ?: return null + val debuggerSession = process.debuggerSession + + val sessionStateFlow = debuggerSession.sessionStateFlow + return JavaDebuggerSessionDto( + isAttachedInitial = debuggerSession.isAttached, + isAttachedFlow = sessionStateFlow.map { DebuggerSession.isAttached(it) }.toRpc(), + ) + } +} diff --git a/java/debugger/backend/src/com/intellij/java/debugger/impl/backend/BackendJavaDebuggerRemoteApiProvider.kt b/java/debugger/backend/src/com/intellij/java/debugger/impl/backend/BackendJavaDebuggerRemoteApiProvider.kt index 84eb4e6daf8b..a4d4eebd4436 100644 --- a/java/debugger/backend/src/com/intellij/java/debugger/impl/backend/BackendJavaDebuggerRemoteApiProvider.kt +++ b/java/debugger/backend/src/com/intellij/java/debugger/impl/backend/BackendJavaDebuggerRemoteApiProvider.kt @@ -2,6 +2,7 @@ package com.intellij.java.debugger.impl.backend import com.intellij.java.debugger.impl.shared.rpc.JavaDebuggerLuxActionsApi +import com.intellij.java.debugger.impl.shared.rpc.JavaDebuggerManagerApi import com.intellij.java.debugger.impl.shared.rpc.JavaDebuggerSessionApi import com.intellij.platform.rpc.backend.RemoteApiProvider import fleet.rpc.remoteApiDescriptor @@ -14,5 +15,8 @@ private class BackendJavaDebuggerRemoteApiProvider : RemoteApiProvider { remoteApi(remoteApiDescriptor()) { BackendJavaDebuggerSessionApi() } + remoteApi(remoteApiDescriptor()) { + BackendJavaDebuggerManagerApi() + } } } \ No newline at end of file diff --git a/java/debugger/impl/src/com/intellij/debugger/impl/DebuggerSession.java b/java/debugger/impl/src/com/intellij/debugger/impl/DebuggerSession.java index 5b3f5fe73b17..9586584b5e02 100644 --- a/java/debugger/impl/src/com/intellij/debugger/impl/DebuggerSession.java +++ b/java/debugger/impl/src/com/intellij/debugger/impl/DebuggerSession.java @@ -48,6 +48,7 @@ import com.intellij.util.ui.UIUtil; import com.intellij.xdebugger.AbstractDebuggerSession; import com.intellij.xdebugger.XDebugSession; import com.intellij.xdebugger.XSourcePosition; +import com.intellij.xdebugger.impl.CoroutineUtilsKt; import com.intellij.xdebugger.impl.XDebuggerManagerImpl; import com.intellij.xdebugger.impl.actions.XDebuggerActions; import com.intellij.xdebugger.impl.evaluate.ValueLookupManagerController; @@ -55,6 +56,9 @@ import com.sun.jdi.ObjectCollectedException; import com.sun.jdi.ThreadReference; import com.sun.jdi.request.EventRequest; import com.sun.jdi.request.StepRequest; +import kotlinx.coroutines.flow.Flow; +import kotlinx.coroutines.flow.MutableStateFlow; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -75,7 +79,8 @@ public final class DebuggerSession implements AbstractDebuggerSession { private volatile int myIgnoreFiltersFrameCountThreshold = 0; - private DebuggerSessionState myState; + private final MutableStateFlow myStateFlow = + CoroutineUtilsKt.createMutableStateFlow(new DebuggerSessionState(State.STOPPED, null)); private final String mySessionName; private final DebugProcessImpl myDebugProcess; @@ -136,6 +141,10 @@ public final class DebuggerSession implements AbstractDebuggerSession { myModifiedClassesScanRequired = modifiedClassesScanRequired; } + public Flow getSessionStateFlow() { + return CoroutineUtilsKt.mapFlow(myStateFlow, (sessionState) -> sessionState.myState); + } + private class MyDebuggerStateManager extends DebuggerStateManager { private DebuggerContextImpl myDebuggerContext; @@ -170,7 +179,7 @@ public final class DebuggerSession implements AbstractDebuggerSession { LOG.debug("DebuggerSession state = " + state + ", event = " + event); } - myState = new DebuggerSessionState(state, description); + myStateFlow.setValue(new DebuggerSessionState(state, description)); fireStateChanged(context, event); }; @@ -213,7 +222,6 @@ public final class DebuggerSession implements AbstractDebuggerSession { myDebugProcess = debugProcess; SESSION_EMPTY_CONTEXT = DebuggerContextImpl.createDebuggerContext(this, null, null, null); myContextManager = new MyDebuggerStateManager(); - myState = new DebuggerSessionState(State.STOPPED, null); myDebugProcess.addDebugProcessListener(new MyDebugProcessListener(debugProcess)); ValueLookupManagerController.getInstance(getProject()).startListening(); myDebugEnvironment = environment; @@ -263,15 +271,16 @@ public final class DebuggerSession implements AbstractDebuggerSession { } public State getState() { - return myState.myState; + return myStateFlow.getValue().myState; } public @NlsContexts.Label String getStateDescription() { - if (myState.myDescription != null) { - return myState.myDescription; + String stateDescription = myStateFlow.getValue().myDescription; + if (stateDescription != null) { + return stateDescription; } - return switch (myState.myState) { + return switch (getState()) { case STOPPED, DISPOSED -> JavaDebuggerBundle.message("status.debug.stopped"); case RUNNING -> JavaDebuggerBundle.message("status.app.running"); case IN_STEPPING -> JavaDebuggerBundle.message("status.app.stepping"); @@ -451,7 +460,12 @@ public final class DebuggerSession implements AbstractDebuggerSession { } public boolean isAttached() { - return !isStopped() && getState() != State.WAITING_ATTACH; + return isAttached(getState()); + } + + @ApiStatus.Internal + public static boolean isAttached(State state) { + return state != State.STOPPED && state != State.WAITING_ATTACH; } @Override diff --git a/java/debugger/shared/intellij.java.debugger.impl.shared.iml b/java/debugger/shared/intellij.java.debugger.impl.shared.iml index b6626865c61f..69cd762c70ed 100644 --- a/java/debugger/shared/intellij.java.debugger.impl.shared.iml +++ b/java/debugger/shared/intellij.java.debugger.impl.shared.iml @@ -48,5 +48,6 @@ + \ No newline at end of file diff --git a/java/debugger/shared/resources/intellij.java.debugger.impl.shared.xml b/java/debugger/shared/resources/intellij.java.debugger.impl.shared.xml index 320654126c0d..73958a6855d8 100644 --- a/java/debugger/shared/resources/intellij.java.debugger.impl.shared.xml +++ b/java/debugger/shared/resources/intellij.java.debugger.impl.shared.xml @@ -6,6 +6,7 @@ + diff --git a/java/debugger/shared/src/com/intellij/java/debugger/impl/shared/SharedDebuggerInitializationProjectActivity.kt b/java/debugger/shared/src/com/intellij/java/debugger/impl/shared/SharedDebuggerInitializationProjectActivity.kt new file mode 100644 index 000000000000..1977f8947d9b --- /dev/null +++ b/java/debugger/shared/src/com/intellij/java/debugger/impl/shared/SharedDebuggerInitializationProjectActivity.kt @@ -0,0 +1,11 @@ +// 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.java.debugger.impl.shared + +import com.intellij.openapi.project.Project +import com.intellij.openapi.startup.ProjectActivity + +private class SharedDebuggerInitializationProjectActivity : ProjectActivity { + override suspend fun execute(project: Project) { + SharedJavaDebuggerManager.getInstance(project) + } +} diff --git a/java/debugger/shared/src/com/intellij/java/debugger/impl/shared/SharedJavaDebuggerManager.kt b/java/debugger/shared/src/com/intellij/java/debugger/impl/shared/SharedJavaDebuggerManager.kt new file mode 100644 index 000000000000..6af257d8da1d --- /dev/null +++ b/java/debugger/shared/src/com/intellij/java/debugger/impl/shared/SharedJavaDebuggerManager.kt @@ -0,0 +1,70 @@ +// 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.java.debugger.impl.shared + +import com.intellij.java.debugger.impl.shared.rpc.JavaDebuggerManagerApi +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.platform.util.coroutines.childScope +import com.intellij.xdebugger.impl.FrontendXDebuggerManagerListener +import com.intellij.xdebugger.impl.frame.XDebugManagerProxy +import com.intellij.xdebugger.impl.rpc.XDebugSessionDto +import com.intellij.xdebugger.impl.rpc.XDebugSessionId +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import org.jetbrains.annotations.ApiStatus + +@Service(Service.Level.PROJECT) +@ApiStatus.Internal +class SharedJavaDebuggerManager(private val project: Project, private val cs: CoroutineScope) { + private val synchronousExecutor = Channel Unit>(capacity = Integer.MAX_VALUE) + private val debugSessions = hashMapOf() + + init { + cs.launch { + for (event in synchronousExecutor) { + event() + } + } + cs.launch { + XDebugManagerProxy.getInstance().getCurrentSessionFlow(project).collectLatest { proxy -> + if (proxy == null) return@collectLatest + synchronousExecutor.send { + val sessionId = proxy.id + if (sessionId in debugSessions) return@send + requestJavaSession(sessionId) + } + } + } + project.messageBus.connect(cs).subscribe(FrontendXDebuggerManagerListener.TOPIC, object : FrontendXDebuggerManagerListener { + override fun processStopped(sessionId: XDebugSessionId) { + synchronousExecutor.trySend { + debugSessions.remove(sessionId)?.close() + } + } + + override fun processStarted(sessionId: XDebugSessionId, sessionDto: XDebugSessionDto) { + synchronousExecutor.trySend { + requestJavaSession(sessionId) + } + } + }) + } + + private suspend fun requestJavaSession(sessionId: XDebugSessionId) { + val dto = JavaDebuggerManagerApi.getInstance().getJavaSession(sessionId) ?: return + val scope = cs.childScope("Java session $sessionId") + debugSessions.put(sessionId, SharedJavaDebuggerSession(dto, scope))?.close() + } + + fun getJavaSession(sessionId: XDebugSessionId): SharedJavaDebuggerSession? { + return debugSessions[sessionId] + } + + companion object { + @JvmStatic + fun getInstance(project: Project): SharedJavaDebuggerManager = project.service() + } +} diff --git a/java/debugger/shared/src/com/intellij/java/debugger/impl/shared/SharedJavaDebuggerSession.kt b/java/debugger/shared/src/com/intellij/java/debugger/impl/shared/SharedJavaDebuggerSession.kt new file mode 100644 index 000000000000..975f975de136 --- /dev/null +++ b/java/debugger/shared/src/com/intellij/java/debugger/impl/shared/SharedJavaDebuggerSession.kt @@ -0,0 +1,22 @@ +// 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.java.debugger.impl.shared + +import com.intellij.java.debugger.impl.shared.rpc.JavaDebuggerSessionDto +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn +import org.jetbrains.annotations.ApiStatus + +@ApiStatus.Internal +class SharedJavaDebuggerSession(dto: JavaDebuggerSessionDto, private val cs: CoroutineScope) { + private val isAttachedFlow = dto.isAttachedFlow.toFlow() + .stateIn(cs, SharingStarted.Eagerly, dto.isAttachedInitial) + + val isAttached: Boolean get() = isAttachedFlow.value + + + fun close() { + cs.cancel() + } +} diff --git a/java/debugger/shared/src/com/intellij/java/debugger/impl/shared/actions/ThreadDumpAction.kt b/java/debugger/shared/src/com/intellij/java/debugger/impl/shared/actions/ThreadDumpAction.kt index aa5bc4ede7b9..9529627a7df0 100644 --- a/java/debugger/shared/src/com/intellij/java/debugger/impl/shared/actions/ThreadDumpAction.kt +++ b/java/debugger/shared/src/com/intellij/java/debugger/impl/shared/actions/ThreadDumpAction.kt @@ -6,6 +6,7 @@ import com.intellij.frontend.FrontendApplicationInfo import com.intellij.frontend.FrontendType import com.intellij.ide.ui.icons.icon import com.intellij.java.debugger.impl.shared.SharedDebuggerUtils +import com.intellij.java.debugger.impl.shared.SharedJavaDebuggerManager import com.intellij.java.debugger.impl.shared.rpc.JavaDebuggerSessionApi import com.intellij.java.debugger.impl.shared.rpc.JavaThreadDumpDto import com.intellij.java.debugger.impl.shared.rpc.JavaThreadDumpItemDto @@ -62,8 +63,8 @@ private class ThreadDumpAction : DumbAwareAction(), ActionRemoteBehaviorSpecific presentation.setEnabled(false) return } - // TODO should be isAttached - val isAttached = !sessionProxy.isStopped + val javaSession = SharedJavaDebuggerManager.getInstance(project).getJavaSession(sessionProxy.id) + val isAttached = javaSession != null && javaSession.isAttached presentation.setEnabled(isAttached) } diff --git a/java/debugger/shared/src/com/intellij/java/debugger/impl/shared/rpc/JavaDebuggerManagerApi.kt b/java/debugger/shared/src/com/intellij/java/debugger/impl/shared/rpc/JavaDebuggerManagerApi.kt new file mode 100644 index 000000000000..8b62eb01866a --- /dev/null +++ b/java/debugger/shared/src/com/intellij/java/debugger/impl/shared/rpc/JavaDebuggerManagerApi.kt @@ -0,0 +1,31 @@ +// 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.java.debugger.impl.shared.rpc + +import com.intellij.platform.rpc.RemoteApiProviderService +import com.intellij.xdebugger.impl.rpc.XDebugSessionId +import fleet.rpc.RemoteApi +import fleet.rpc.Rpc +import fleet.rpc.core.RpcFlow +import fleet.rpc.remoteApiDescriptor +import kotlinx.serialization.Serializable +import org.jetbrains.annotations.ApiStatus + +@ApiStatus.Internal +@Rpc +interface JavaDebuggerManagerApi : RemoteApi { + suspend fun getJavaSession(sessionId: XDebugSessionId): JavaDebuggerSessionDto? + + companion object { + @JvmStatic + suspend fun getInstance(): JavaDebuggerManagerApi { + return RemoteApiProviderService.resolve(remoteApiDescriptor()) + } + } +} + +@ApiStatus.Internal +@Serializable +data class JavaDebuggerSessionDto( + val isAttachedInitial: Boolean, + val isAttachedFlow: RpcFlow, +) diff --git a/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/coroutineUtils.kt b/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/coroutineUtils.kt index 5b557fa2c438..f9bdd94c3c2f 100644 --- a/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/coroutineUtils.kt +++ b/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/coroutineUtils.kt @@ -14,9 +14,7 @@ import com.intellij.xdebugger.impl.frame.XDebugManagerProxy import com.intellij.xdebugger.impl.rpc.XDebuggerManagerApi import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -25,6 +23,8 @@ internal fun createMutableStateFlow(initialValue: T): MutableStateFlow { return MutableStateFlow(initialValue) } +internal fun mapFlow(f: Flow, mapper: (T) -> R): Flow = f.map(mapper) + // Used only for Java code, since MutableSharedFlow function cannot be called there. internal fun createMutableSharedFlow(replay: Int, extraBufferCapacity: Int): MutableSharedFlow { return MutableSharedFlow(replay, extraBufferCapacity)