jetbrains-rider/src-203-212/software/aws/toolkits/jetbrains/services/lambda/dotnet/DotnetDebugUtils.kt (175 lines of code) (raw):
// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package software.aws.toolkits.jetbrains.services.lambda.dotnet
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.filters.TextConsoleBuilderFactory
import com.intellij.execution.process.OSProcessHandler
import com.intellij.execution.process.ProcessHandlerFactory
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.rd.defineNestedLifetime
import com.intellij.xdebugger.XDebugProcessStarter
import com.jetbrains.rd.framework.IdKind
import com.jetbrains.rd.framework.Identities
import com.jetbrains.rd.framework.Protocol
import com.jetbrains.rd.framework.Serializers
import com.jetbrains.rd.framework.SocketWire
import com.jetbrains.rd.util.lifetime.isAlive
import com.jetbrains.rd.util.lifetime.onTermination
import com.jetbrains.rd.util.put
import com.jetbrains.rd.util.reactive.adviseUntil
import com.jetbrains.rdclient.protocol.RdDispatcher
import com.jetbrains.rider.debugger.DebuggerWorkerProcessHandler
import com.jetbrains.rider.debugger.RiderDebuggerWorkerModelManager
import com.jetbrains.rider.model.debuggerWorker.DotNetDebuggerSessionModel
import com.jetbrains.rider.model.debuggerWorkerConnectionHelperModel
import com.jetbrains.rider.projectView.solution
import com.jetbrains.rider.run.IDebuggerOutputListener
import com.jetbrains.rider.run.bindToSettings
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.jetbrains.concurrency.AsyncPromise
import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineUiContext
import software.aws.toolkits.jetbrains.core.utils.buildList
import software.aws.toolkits.jetbrains.services.lambda.dotnet.FindDockerContainer.Companion.DOCKER_CONTAINER
import software.aws.toolkits.jetbrains.services.lambda.dotnet.FindPid.Companion.DOTNET_PID
import software.aws.toolkits.jetbrains.services.lambda.execution.sam.SamDebugSupport
import software.aws.toolkits.jetbrains.utils.DotNetDebuggerUtils
import software.aws.toolkits.jetbrains.utils.compatability.createNetCoreAttachStartInfo
import software.aws.toolkits.jetbrains.utils.execution.steps.Context
import software.aws.toolkits.resources.message
import java.net.InetAddress
import java.util.Timer
import kotlin.concurrent.schedule
object DotnetDebugUtils {
private val LOG = getLogger<DotnetDebugUtils>()
private const val DEBUGGER_MODE = "server"
private const val REMOTE_DEBUGGER_DIR = "/tmp/lambci_debug_files"
const val NUMBER_OF_DEBUG_PORTS = 2
fun createDebugProcess(
environment: ExecutionEnvironment,
debugHost: String,
debugPorts: List<Int>,
context: Context
): XDebugProcessStarter {
val frontendPort = debugPorts[0]
val backendPort = debugPorts[1]
val promise = AsyncPromise<XDebugProcessStarter>()
val edtContext = getCoroutineUiContext()
val bgContext = getCoroutineBgContext()
// Define a debugger lifetime to be able to dispose the debugger process and all nested component on termination
// Using the environment as the disposable root seems to make 2nd usage of debug to fail since the lifetime is already terminated
val debuggerLifetimeDefinition = environment.project.defineNestedLifetime()
val debuggerLifetime = debuggerLifetimeDefinition.lifetime
val scheduler = RdDispatcher(debuggerLifetime)
val debugHostAddress = InetAddress.getByName(debugHost)
val protocol = Protocol(
name = environment.runProfile.name,
serializers = Serializers(),
identity = Identities(IdKind.Client),
scheduler = scheduler,
wire = SocketWire.Client(debuggerLifetime, scheduler, hostAddress = debugHostAddress, port = frontendPort, optId = "FrontendToDebugWorker"),
lifetime = debuggerLifetime
)
val workerModel = runBlocking(edtContext) {
RiderDebuggerWorkerModelManager.createDebuggerModel(debuggerLifetime, protocol)
}
disposableCoroutineScope(environment, environment.runProfile.name).launch(bgContext) {
try {
val dockerContainer = context.getRequiredAttribute(DOCKER_CONTAINER)
val pid = context.getRequiredAttribute(DOTNET_PID)
val riderDebuggerProcessHandler = startDebugWorker(dockerContainer, backendPort, frontendPort)
withContext(edtContext) {
protocol.wire.connected.adviseUntil(debuggerLifetime) connected@{ isConnected ->
if (!isConnected) {
return@connected false
}
workerModel.initialized.adviseUntil(debuggerLifetime) initialized@{ isInitialized ->
if (!isInitialized) {
return@initialized false
}
// Fire backend to connect to debugger.
environment.project.solution.debuggerWorkerConnectionHelperModel.ports.put(
debuggerLifetime,
environment.executionId,
backendPort
)
val startInfo = createNetCoreAttachStartInfo(pid)
val sessionModel = DotNetDebuggerSessionModel(startInfo)
sessionModel.sessionProperties.bindToSettings(debuggerLifetime).apply {
enableHeuristicPathResolve.set(true)
}
workerModel.activeSession.set(sessionModel)
val console = TextConsoleBuilderFactory.getInstance().createBuilder(environment.project).console
promise.setResult(
DotNetDebuggerUtils.createAndStartSession(
executionConsole = console,
env = environment,
sessionLifetime = debuggerLifetime,
processHandler = DebuggerWorkerProcessHandler(
riderDebuggerProcessHandler,
workerModel,
true,
riderDebuggerProcessHandler.commandLine,
debuggerLifetime
),
protocol = protocol,
sessionModel = sessionModel,
outputEventsListener = object : IDebuggerOutputListener {}
)
)
return@initialized true
}
return@connected true
}
}
} catch (t: Throwable) {
LOG.warn(t) { "Failed to start debugger" }
debuggerLifetimeDefinition.terminate(true)
withContext(edtContext) {
promise.setError(t)
}
}
}
val checkDebuggerTask = Timer("Debugger Worker launch timer", true).schedule(SamDebugSupport.debuggerConnectTimeoutMs()) {
if (debuggerLifetimeDefinition.isAlive && !protocol.wire.connected.value) {
runBlocking(edtContext) {
debuggerLifetimeDefinition.terminate()
promise.setError(message("lambda.debug.process.start.timeout"))
}
}
}
debuggerLifetime.onTermination {
checkDebuggerTask.cancel()
}
return promise.get()!!
}
private fun startDebugWorker(dockerContainer: String, backendPort: Int, frontendPort: Int): OSProcessHandler {
val dockerPrelude = buildList<String> {
add("docker")
add("exec")
add("-i")
if (ApplicationManager.getApplication().isUnitTestMode) {
add("--env")
add("RIDER_DEBUGGER_LOG_DIR=/tmp/logs/")
}
add(dockerContainer)
}.toTypedArray()
val runDebuggerCommand = GeneralCommandLine(
*dockerPrelude,
// use dotnet binary bundled with worker since Lambda netcore2.1 image seems to be missing:
// System.Runtime.CompilerServices.TupleElementNamesAttribute' from assembly 'mscorlib, Version=4.0.0.0
// and therefore cannot debug netcore2.1 under Rider 2021.2+
"$REMOTE_DEBUGGER_DIR/linux-x64/dotnet/dotnet",
"$REMOTE_DEBUGGER_DIR/${DotNetDebuggerUtils.debuggerAssemblyFile.name}",
"--mode=$DEBUGGER_MODE",
"--frontend-port=$frontendPort",
"--backend-port=$backendPort"
)
LOG.debug {
"""
Starting Rider debug worker for $dockerContainer with backend $backendPort and frontend $frontendPort
Command: ${runDebuggerCommand.commandLineString}
""".trimIndent()
}
return ProcessHandlerFactory.getInstance().createProcessHandler(runDebuggerCommand)
}
}