[rd debugger] IDEA-370752 Pass isAttached state to FE

GitOrigin-RevId: 6c7beedb9a6dae542b709e4f25ff5435bdf33319
This commit is contained in:
Maksim Zuev
2025-04-16 20:14:15 +00:00
committed by intellij-monorepo-bot
parent 00751a99ef
commit 6cf266d548
11 changed files with 193 additions and 13 deletions
@@ -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(),
)
}
}
@@ -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<JavaDebuggerSessionApi>()) {
BackendJavaDebuggerSessionApi()
}
remoteApi(remoteApiDescriptor<JavaDebuggerManagerApi>()) {
BackendJavaDebuggerManagerApi()
}
}
}
@@ -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<DebuggerSessionState> 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<State> 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
@@ -48,5 +48,6 @@
<orderEntry type="module" module-name="intellij.platform.execution" />
<orderEntry type="module" module-name="intellij.platform.execution.impl" />
<orderEntry type="module" module-name="intellij.java.frontback.impl" />
<orderEntry type="module" module-name="intellij.platform.util.coroutines" />
</component>
</module>
@@ -6,6 +6,7 @@
<rdct.remoteSettingProvider implementation="com.intellij.debugger.settings.JavaDebuggerSettingsProvider"/>
<xdebugger.xValueCustomDescriptorSerializerProvider
implementation="com.intellij.java.debugger.impl.shared.engine.JavaValueDescriptorSerializerProvider"/>
<postStartupActivity implementation="com.intellij.java.debugger.impl.shared.SharedDebuggerInitializationProjectActivity"/>
</extensions>
<actions resource-bundle="messages.JavaDebuggerSharedBundle">
<action id="DumpThreads" class="com.intellij.java.debugger.impl.shared.actions.ThreadDumpAction" icon="AllIcons.Actions.Dump">
@@ -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)
}
}
@@ -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<suspend () -> Unit>(capacity = Integer.MAX_VALUE)
private val debugSessions = hashMapOf<XDebugSessionId, SharedJavaDebuggerSession>()
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()
}
}
@@ -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()
}
}
@@ -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)
}
@@ -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<Unit> {
suspend fun getJavaSession(sessionId: XDebugSessionId): JavaDebuggerSessionDto?
companion object {
@JvmStatic
suspend fun getInstance(): JavaDebuggerManagerApi {
return RemoteApiProviderService.resolve(remoteApiDescriptor<JavaDebuggerManagerApi>())
}
}
}
@ApiStatus.Internal
@Serializable
data class JavaDebuggerSessionDto(
val isAttachedInitial: Boolean,
val isAttachedFlow: RpcFlow<Boolean>,
)
@@ -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 <T> createMutableStateFlow(initialValue: T): MutableStateFlow<T> {
return MutableStateFlow(initialValue)
}
internal fun <T, R> mapFlow(f: Flow<T>, mapper: (T) -> R): Flow<R> = f.map(mapper)
// Used only for Java code, since MutableSharedFlow function cannot be called there.
internal fun <T> createMutableSharedFlow(replay: Int, extraBufferCapacity: Int): MutableSharedFlow<T> {
return MutableSharedFlow(replay, extraBufferCapacity)