jetbrains-rider/src-203-212/software/aws/toolkits/jetbrains/services/clouddebug/DotNetDebuggerSupport.kt (324 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.clouddebug
import com.intellij.execution.configurations.RuntimeConfigurationError
import com.intellij.execution.filters.TextConsoleBuilderFactory
import com.intellij.execution.process.ProcessAdapter
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.execution.ui.RunContentDescriptor
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.rd.defineNestedLifetime
import com.intellij.openapi.util.io.FileUtil
import com.intellij.xdebugger.XDebugProcessStarter
import com.intellij.xdebugger.XDebuggerManager
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.framework.impl.RpcTimeouts
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.RiderEnvironment
import com.jetbrains.rider.debugger.DebuggerHelperHost
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 org.jetbrains.concurrency.AsyncPromise
import org.jetbrains.concurrency.Promise
import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info
import software.aws.toolkits.core.utils.trace
import software.aws.toolkits.jetbrains.services.clouddebug.execution.steps.ResourceTransferStep
import software.aws.toolkits.jetbrains.services.ecs.execution.ImmutableContainerOptions
import software.aws.toolkits.jetbrains.utils.DotNetDebuggerUtils
import software.aws.toolkits.jetbrains.utils.DotNetRuntimeUtils
import software.aws.toolkits.jetbrains.utils.compatability.createNetCoreStartInfo
import software.aws.toolkits.jetbrains.utils.execution.steps.Context
import software.aws.toolkits.resources.message
import java.io.File
import java.io.FileNotFoundException
import java.io.OutputStream
import java.util.Timer
import java.util.concurrent.CompletableFuture
import kotlin.concurrent.schedule
class DotNetDebuggerSupport : DebuggerSupport() {
companion object {
private val logger = getLogger<DotNetDebuggerSupport>()
private const val DOTNET_EXECUTABLE = "dotnet"
private const val START_COMMAND_ASSEMBLY_PLACEHOLDER = "$DOTNET_EXECUTABLE <path_to_assembly>"
private const val DEBUGGER_MODE = "server"
}
private var exeRemotePath: String = ""
override val numberOfDebugPorts: Int
get() = 2
override val platform: CloudDebuggingPlatform
get() = CloudDebuggingPlatform.DOTNET
private var localDebuggerPath: String = ""
override fun startupCommand(): CloudDebugStartupCommand = DotNetStartupCommand()
override val debuggerPath = object : DebuggerPath {
override fun getDebuggerPath(): String = localDebuggerPath
override fun getDebuggerEntryPoint(): String =
"${getRemoteDebuggerPath()}/${DotNetDebuggerUtils.cloudDebuggerTempDirName}/${DotNetDebuggerUtils.debuggerAssemblyFile.name}"
// TODO fix when cloud-debug fixes rsync to run mkdir -p
override fun getRemoteDebuggerPath(): String =
// "/aws/cloud-debug/debugger/$platform"
"/aws/$platform"
}
override fun attachDebugger(
context: Context,
containerName: String,
containerOptions: ImmutableContainerOptions,
environment: ExecutionEnvironment,
ports: List<Int>,
displayName: String
): CompletableFuture<RunContentDescriptor?> {
val manager = XDebuggerManager.getInstance(environment.project)
val future = CompletableFuture<RunContentDescriptor?>()
if (exeRemotePath.isEmpty()) {
future.completeExceptionally(RuntimeException("DotNet executable to debug is not specified"))
return future
}
if (ports.size < 2) {
future.completeExceptionally(RuntimeException("DotNet requires two ports to be specified"))
return future
}
val frontendPort = ports[0]
val backendPort = ports[1]
runInEdt {
try {
createDebugProcessAsync(environment, frontendPort, backendPort, exeRemotePath).then {
it?.let {
future.complete(
manager.startSessionAndShowTab(
displayName,
null,
it
).runContentDescriptor
)
} ?: run {
future.complete(null)
}
}
} catch (e: Exception) {
future.completeExceptionally(e)
}
}
return future
}
override fun createDebuggerUploadStep(context: Context, containerName: String): ResourceTransferStep {
val debuggerAssemblyNames = DebuggerHelperHost.getInstance(context.getRequiredAttribute(Context.PROJECT_ATTRIBUTE))
.model.getDebuggerAssemblies.sync(Unit, RpcTimeouts.longRunning)
// Helper assembly to detect dbgshim on 192 Rider
val tempDirectory = FileUtil.getTempDirectory()
val debuggerLocalTemp = File(tempDirectory, DotNetDebuggerUtils.cloudDebuggerTempDirName)
if (debuggerLocalTemp.exists()) {
FileUtil.delete(debuggerLocalTemp)
}
debuggerLocalTemp.mkdirs()
localDebuggerPath = debuggerLocalTemp.canonicalPath
prepareDebuggerArtifacts(debuggerLocalTemp, debuggerAssemblyNames.toTypedArray())
val remoteDebuggerPath = debuggerPath.getRemoteDebuggerPath()
return ResourceTransferStep(
localPath = localDebuggerPath,
remotePath = remoteDebuggerPath,
containerName = containerName
)
}
override fun automaticallyAugmentable(input: List<String>): Boolean {
if (input.first().trim() != DOTNET_EXECUTABLE) {
throw RuntimeConfigurationError(message("cloud_debug.run_configuration.dotnet.start_command.miss_runtime", DOTNET_EXECUTABLE))
}
val restCommands = input.drop(1)
val path = restCommands.firstOrNull()?.trim()
?: throw RuntimeConfigurationError(
message("cloud_debug.run_configuration.dotnet.start_command.miss_assembly_path", START_COMMAND_ASSEMBLY_PLACEHOLDER)
)
if (!path.contains('/')) {
throw RuntimeConfigurationError(message("cloud_debug.run_configuration.dotnet.start_command.assembly_path_not_valid", path))
}
// Start command should follow the pattern 'dotnet <remote_path_to_assembly>'. Take a remote assembly path.
exeRemotePath = path
return true
}
override fun attachDebuggingArguments(input: List<String>, ports: List<Int>, debuggerPath: String): String {
val debuggerRemoteDirPath = "${this.debuggerPath.getRemoteDebuggerPath()}/${DotNetDebuggerUtils.cloudDebuggerTempDirName}"
val remoteDebuggerLogPath = "$debuggerRemoteDirPath/Logs"
if (ports.size < 2) {
val message = message("cloud_debug.step.dotnet.two_ports_required")
logger.debug { message }
throw IllegalStateException(message)
}
val frontendPort = ports[0]
val backendPort = ports[1]
val debugArgs = StringBuilder()
.append("RESHARPER_HOST_LOG_DIR=$remoteDebuggerLogPath ")
.append("dotnet ")
.append("$debuggerPath ")
.append("--mode=$DEBUGGER_MODE ")
.append("--frontend-port=$frontendPort ")
.append("--backend-port=$backendPort")
logger.info { "Attaching default Rider Debugger arguments" }
return debugArgs.toString()
}
private fun createDebugProcessAsync(
environment: ExecutionEnvironment,
frontendPort: Int,
backendPort: Int,
exeRemotePath: String
): Promise<XDebugProcessStarter?> {
val promise = AsyncPromise<XDebugProcessStarter?>()
// Define a debugger lifetime to be able to dispose the debugger process and all nested component on termination
val debuggerLifetimeDefinition = environment.defineNestedLifetime()
val debuggerLifetime = debuggerLifetimeDefinition.lifetime
val scheduler = RdDispatcher(debuggerLifetime)
val startInfo = createNetCoreStartInfo(
exePath = exeRemotePath
)
val protocol = Protocol(
name = "",
serializers = Serializers(),
identity = Identities(IdKind.Client),
scheduler = scheduler,
wire = SocketWire.Client(
lifetime = debuggerLifetime,
scheduler = scheduler,
port = frontendPort,
optId = "FrontendToDebugWorker"
),
lifetime = debuggerLifetime
)
protocol.wire.connected.adviseUntil(debuggerLifetime) connected@{ isConnected ->
if (!isConnected) {
return@connected false
}
try {
val workerModel = RiderDebuggerWorkerModelManager.createDebuggerModel(debuggerLifetime, protocol)
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 sessionModel = DotNetDebuggerSessionModel(startInfo)
sessionModel.sessionProperties.bindToSettings(debuggerLifetime).apply {
enableHeuristicPathResolve.set(true)
remoteDebug.set(true)
}
workerModel.activeSession.set(sessionModel)
val console = TextConsoleBuilderFactory.getInstance().createBuilder(environment.project).console
val processHandler = object : ProcessHandler() {
override fun detachProcessImpl() {
destroyProcessImpl()
}
override fun detachIsDefault(): Boolean = false
override fun getProcessInput(): OutputStream? = null
override fun destroyProcessImpl() {
notifyProcessTerminated(0)
}
fun notifyProcessDestroyed(exitCode: Int) {
notifyProcessTerminated(exitCode)
}
}
processHandler.addProcessListener(
object : ProcessAdapter() {
override fun processTerminated(event: ProcessEvent) {
logger.trace { "Process exited. Terminating debugger lifetime" }
runInEdt {
debuggerLifetimeDefinition.terminate()
}
}
}
)
workerModel.targetExited.advise(debuggerLifetime) {
logger.trace { "Target exited" }
// We should try to kill deployment there because it's already stopped,
// just notify debugger session about termination via its process handler.
processHandler.notifyProcessDestroyed(it.exitCode ?: 0)
}
promise.setResult(
DotNetDebuggerUtils.createAndStartSession(
executionConsole = console,
env = environment,
sessionLifetime = debuggerLifetime,
processHandler = processHandler,
protocol = protocol,
sessionModel = sessionModel,
outputEventsListener = object : IDebuggerOutputListener {}
)
)
return@initialized true
}
} catch (t: Throwable) {
debuggerLifetimeDefinition.terminate()
promise.setError(t)
}
return@connected true
}
val checkDebuggerTask = Timer("Debugger Worker launch timer", true).schedule(60_000L) {
if (debuggerLifetimeDefinition.isAlive && !protocol.wire.connected.value) {
debuggerLifetimeDefinition.terminate()
promise.setError(message("lambda.debug.process.start.timeout"))
}
}
debuggerLifetime.onTermination {
checkDebuggerTask.cancel()
}
return promise
}
private fun prepareDebuggerArtifacts(targetPath: File, assemblyNames: Array<String>) {
val assemblyFileErrors = mutableListOf<String>()
for (assemblyName in assemblyNames) {
val assemblyFile =
try {
// Look through DLL file
val dllFileName = "$assemblyName.dll"
RiderEnvironment.getBundledFile(dllFileName)
} catch (e: FileNotFoundException) {
// Look through EXE file
val exeFileName = "$assemblyName.exe"
try {
RiderEnvironment.getBundledFile(exeFileName)
} catch (e: FileNotFoundException) {
null
}
}
if (assemblyFile == null) {
assemblyFileErrors.add(assemblyName)
logger.trace { "Cannot find assembly with name: '$assemblyName'" }
continue
}
FileUtil.copy(assemblyFile, File(targetPath, assemblyFile.name))
logger.trace { "Copy '${assemblyFile.canonicalPath}'" }
// Check runtimeconfig.json
val runtimeFileName = "$assemblyName.runtimeconfig.json"
try {
val runtimeFile = RiderEnvironment.getBundledFile(runtimeFileName)
// overwrite runtimeconfig of Rider assemblies with own ones which target netcoreapp2.1 instead of 3.0
FileUtil.writeToFile(File(targetPath, runtimeFile.name), DotNetRuntimeUtils.RUNTIME_CONFIG_JSON_21, false)
logger.trace { "Write '${runtimeFile.canonicalPath}'" }
} catch (e: FileNotFoundException) {
logger.trace { "Cannot find '$runtimeFileName'" }
}
// Check deps.json files
val depsFileName = "$assemblyName.deps.json"
try {
val depsFile = RiderEnvironment.getBundledFile(depsFileName)
FileUtil.copy(depsFile, File(targetPath, depsFile.name))
logger.trace { "Copy '${depsFile.canonicalPath}'" }
} catch (e: FileNotFoundException) {
logger.trace { "Cannot find '$depsFileName'" }
}
}
if (assemblyFileErrors.isNotEmpty()) {
throw IllegalStateException("Unable to find necessary assembly with names: '${assemblyFileErrors.joinToString(", ")}'")
}
val linuxSubdirectoryName = "linux-x64"
val linuxMonoSubdirectory = File(targetPath, linuxSubdirectoryName)
if (linuxMonoSubdirectory.isDirectory) {
try {
// remove existing linux Mono distribution since we run debugger on container .net core
linuxMonoSubdirectory.deleteRecursively()
} catch (e: Throwable) {
logger.trace(e) { "Error while trying to delete unused linux Mono directory ${linuxMonoSubdirectory.absolutePath}" }
}
}
}
}