Windows DAP debugger and some misc fixes

This commit is contained in:
FalsePattern 2024-11-07 14:49:43 +01:00
parent f8def6e1de
commit f7014b49eb
Signed by: falsepattern
GPG key ID: E930CDEC50C50E23
16 changed files with 951 additions and 94 deletions

View file

@ -1,9 +1,26 @@
import de.undercouch.gradle.tasks.download.Download
import org.jetbrains.intellij.platform.gradle.Constants
import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType
plugins {
id("de.undercouch.download") version("5.6.0")
}
val lsp4jVersion: String by project
val clionVersion: String by project
val genOutputDir = layout.buildDirectory.dir("generated-resources")
sourceSets["main"].resources.srcDir(genOutputDir)
tasks {
register<Download>("downloadProps") {
src("https://falsepattern.com/zigbrains/msvc.properties")
dest(genOutputDir.map { it.file("msvc.properties") })
}
processResources {
dependsOn("downloadProps")
}
}
dependencies {
intellijPlatform {
create(IntelliJPlatformType.CLion, providers.gradleProperty("clionVersion"))

View file

@ -30,6 +30,7 @@ import java.io.PipedOutputStream
class BlockingPipedInputStream(src: PipedOutputStream, pipeSize: Int) : PipedInputStream(src, pipeSize) {
var closed = false
@Synchronized
override fun read(): Int {
if (closed) {
throw IOException("stream closed")

View file

@ -22,24 +22,31 @@
package com.falsepattern.zigbrains.debugger.dap
import com.falsepattern.zigbrains.project.run.ZigProcessHandler
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.execution.ExecutionException
import com.intellij.execution.configurations.PtyCommandLine
import com.intellij.execution.process.BaseProcessHandler
import com.intellij.execution.process.ProcessAdapter
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessListener
import com.intellij.execution.process.ProcessOutputType
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.KeyWithDefaultValue
import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.util.progress.getMaybeCancellable
import com.intellij.util.system.CpuArch
import com.jetbrains.cidr.ArchitectureType
import com.jetbrains.cidr.execution.Installer
import com.jetbrains.cidr.execution.debugger.CidrDebuggerSettings
import com.jetbrains.cidr.execution.debugger.backend.*
import com.jetbrains.cidr.execution.debugger.memory.Address
import com.jetbrains.cidr.execution.debugger.memory.AddressRange
import com.jetbrains.cidr.system.HostMachine
import com.jetbrains.cidr.system.LocalHost
import io.ktor.util.*
import kotlinx.coroutines.*
import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.future.asDeferred
import org.eclipse.lsp4j.debug.*
import org.eclipse.lsp4j.debug.services.IDebugProtocolClient
@ -47,13 +54,15 @@ import org.eclipse.lsp4j.debug.services.IDebugProtocolServer
import org.eclipse.lsp4j.jsonrpc.MessageConsumer
import org.eclipse.lsp4j.jsonrpc.debug.DebugLauncher
import org.eclipse.lsp4j.jsonrpc.messages.Either3
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.OutputStream
import java.io.PipedOutputStream
import java.lang.Exception
import java.nio.file.Path
import java.util.TreeMap
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executors
import kotlin.io.path.pathString
import kotlin.math.min
abstract class DAPDriver<Server : IDebugProtocolServer, Client : IDebugProtocolClient>(
handler: Handler,
@ -66,6 +75,41 @@ abstract class DAPDriver<Server : IDebugProtocolServer, Client : IDebugProtocolC
protected lateinit var capabilities: Capabilities
protected lateinit var initializeFuture: Job
protected val breakpoints = HashMap<Int, MappedBreakpoint>()
protected val modules = HashMap<Int, MappedModule>()
private val registerSets = TreeMap<String, List<LLValue>>()
@Volatile
private var childProcess: BaseProcessHandler<*>? = null
@Volatile
private var processInput: OutputStream? = null
@Volatile
private var dummyOutput: ByteArrayOutputStream? = ByteArrayOutputStream()
private val multiplexer = object: OutputStream() {
private val inferior get() = processInput ?: dummyOutput
override fun close() {
inferior?.close()
}
override fun flush() {
inferior?.flush()
}
override fun write(b: Int) {
inferior?.write(b)
}
override fun write(b: ByteArray) {
inferior?.write(b)
}
override fun write(b: ByteArray, off: Int, len: Int) {
inferior?.write(b, off, len)
}
}
protected abstract fun createDebuggerClient(): Client
protected abstract fun getServerInterface(): Class<Server>
protected abstract fun wrapMessageConsumer(mc: MessageConsumer): MessageConsumer
@ -213,30 +257,34 @@ abstract class DAPDriver<Server : IDebugProtocolServer, Client : IDebugProtocolC
notSupported()
}
/**
* User presses "Pause Program" button.
* {@link #handleInterrupted} supposed to be called asynchronously when actual pause happened
*/
override fun interrupt(): Boolean {
val pause = PauseArguments()
pause.threadId = -1
server.pause(pause)
server.pause(pause).getSafe()
return true
}
override fun resume(): Boolean {
val args = ContinueArguments()
server.continue_(args)
server.continue_(args).getSafe()
return true
}
@Suppress("OVERRIDE_DEPRECATION")
@Deprecated("Inherited from deprecated")
override fun stepOver(p0: Boolean) {
deprecated()
}
@Suppress("OVERRIDE_DEPRECATION")
@Deprecated("Inherited from deprecated")
override fun stepInto(p0: Boolean, p1: Boolean) {
deprecated()
}
@Suppress("OVERRIDE_DEPRECATION")
@Deprecated("Inherited from deprecated")
override fun stepOut(p0: Boolean) {
deprecated()
}
@ -248,44 +296,57 @@ abstract class DAPDriver<Server : IDebugProtocolServer, Client : IDebugProtocolC
SteppingGranularity.INSTRUCTION
else
SteppingGranularity.LINE
server.next(args)
server.next(args).getSafe()
}
override fun stepInto(thread: LLThread, forceStepIntoFramesWithNoDebugInfo: Boolean, stepByInstruction: Boolean) {
val args = StepInArguments()
args.targetId = Math.toIntExact(thread.id)
args.threadId = Math.toIntExact(thread.id)
args.granularity = if (stepByInstruction)
SteppingGranularity.INSTRUCTION
else
SteppingGranularity.LINE
server.stepIn(args)
server.stepIn(args).getSafe()
}
override fun stepOut(thread: LLThread, stopInFramesWithNoDebugInfo: Boolean) {
val args = StepOutArguments()
args.threadId = Math.toIntExact(thread.id)
server.stepOut(args)
server.stepOut(args).getSafe()
}
/**
* Run to source file line
*
* @see #stepOver
*/
override fun runTo(path: String, line: Int) {
val targetArgs = GotoTargetsArguments()
val src = Util.toSource(path)
targetArgs.source = src
targetArgs.line = line
zigCoroutineScope.launch {
val locations = server.gotoTargets(targetArgs).asDeferred().await()
val targets = locations.targets.firstOrNull() ?: throw RuntimeException("Could not find runTo target!")
val locations = server.gotoTargets(targetArgs).getSafe()
val targets = locations.targets.firstOrNull() ?: throw ExecutionException("Could not find runTo target!")
val args = GotoArguments()
args.targetId = targets.id
args.threadId = -1
server.goto_(args)
}
server.goto_(args).getSafe()
}
override fun runTo(p0: Address) {
/**
* Run to PC address
*
* @see #stepOver
*/
override fun runTo(address: Address) {
throw UnsupportedOperationException("RunTo address not implemented!")
}
/**
* Perform debugger exit
*
* @see #stepOver
*/
override fun doExit(): Boolean {
val disconnectArgs = DisconnectArguments()
disconnectArgs.terminateDebuggee = true
@ -293,12 +354,16 @@ abstract class DAPDriver<Server : IDebugProtocolServer, Client : IDebugProtocolC
return true
}
// TODO
/**
* "Jump" to support
*/
override fun jumpToLine(thread: LLThread, path: String, line: Int, canLeaveFunction: Boolean): StopPlace {
throw DebuggerCommandException("Can't resolve address for line $path:$line")
}
// TODO
/**
* "Jump" to support
*/
override fun jumpToAddress(thread: LLThread, address: Address, canLeaveFunction: Boolean): StopPlace {
throw DebuggerCommandException("Can't jump to address $address")
}
@ -311,10 +376,16 @@ abstract class DAPDriver<Server : IDebugProtocolServer, Client : IDebugProtocolC
addPathMapping(index, from, to)
}
/**
* Autocomplete support for debugger console
*/
override fun completeConsoleCommand(p0: String, p1: Int): ResultList<String> {
throw ExecutionException("completeConsoleCommand")
}
/**
* Watchpoint handling
*/
override fun addWatchpoint(
threadId: Long,
frameIndex: Int,
@ -326,37 +397,16 @@ abstract class DAPDriver<Server : IDebugProtocolServer, Client : IDebugProtocolC
throw ExecutionException("TODO")
}
/**
* Watchpoint handling
*/
override fun removeWatchpoint(ids: MutableList<Int>) {
throw ExecutionException("TODO")
}
data class PathedSourceBreakpoint(
val path: String,
val src: SourceBreakpoint
)
data class MappedBreakpoint(
val id: Int,
val java: LLBreakpoint,
val loc: LLBreakpointLocation?,
val dap: Breakpoint,
val ref: Either3<PathedSourceBreakpoint, FunctionBreakpoint, InstructionBreakpoint>
) {
constructor(dap: Breakpoint, ref: Either3<PathedSourceBreakpoint, FunctionBreakpoint, InstructionBreakpoint>) :
this(dap.id, Util.breakpointJBFromDAP(dap), Util.getLocation(dap), dap, ref)
}
protected val breakpoints = HashMap<Int, MappedBreakpoint>()
data class MappedModule(
val java: LLModule,
val dap: Module
) {
constructor(dap: Module): this(Util.moduleJBFromDAP(dap), dap)
}
protected val modules = HashMap<Int, MappedModule>()
/** User adds a breakpoint
* {@link #handleBreakpointAdded} supposed to be called asynchronously when done
*/
override fun addBreakpoint(path: String, line: Int, condition: String?, ignoreSourceHash: Boolean): AddBreakpointResult {
val bp = SourceBreakpoint()
bp.line = line + 1
@ -387,6 +437,9 @@ abstract class DAPDriver<Server : IDebugProtocolServer, Client : IDebugProtocolC
return res.breakpoints
}
/**
* User adds a symbolic breakpoint
*/
override fun addSymbolicBreakpoint(symBreakpoint: SymbolicBreakpoint): LLSymbolicBreakpoint? {
if (!capabilities.supportsFunctionBreakpoints)
throw DebuggerCommandException("Server doesn't support function breakpoints!")
@ -417,6 +470,9 @@ abstract class DAPDriver<Server : IDebugProtocolServer, Client : IDebugProtocolC
return res.breakpoints
}
/**
* User adds an address breakpoint
*/
override fun addAddressBreakpoint(address: Address, condition: String?): AddBreakpointResult {
if (!capabilities.supportsInstructionBreakpoints)
throw DebuggerCommandException("Server doesn't support instruction breakpoints!")
@ -444,16 +500,601 @@ abstract class DAPDriver<Server : IDebugProtocolServer, Client : IDebugProtocolC
return server.setInstructionBreakpoints(args).getSafe().breakpoints
}
/**
* User removes symbolic or line breakpoint.
*
* [handleBreakpointRemoved] supposed to be called asynchronously when done
*/
override fun removeCodepoints(ids: MutableCollection<Int>) {
val removed = ArrayList<MappedBreakpoint>()
for (id in ids) {
breakpoints.remove(id)?.let { removed.add(it) }
}
val anySrc = removed.any { it.ref.isFirst }
val anyFunc = removed.any { it.ref.isSecond }
val anyAddr = removed.any { it.ref.isThird }
if (anySrc) {
val sources = removed.asSequence().filter { it.ref.isFirst }.map { it.ref.first.path }.distinct().toList()
val bps = breakpoints.values.asSequence().filter { it.ref.isFirst }.map { it.ref.first }.toList()
for (source in sources) {
val sourceBps = bps.asSequence().filter { it.path == source }.map { it.src }.toList()
updateSourceBreakpoints(source, sourceBps)
}
}
if (anyFunc) {
updateSymbolicBreakpoints(breakpoints.values.asSequence().filter { it.ref.isSecond }.map { it.ref.second }.toList())
}
if (anyAddr) {
updateAddressBreakpoints(breakpoints.values.asSequence().filter { it.ref.isThird }.map { it.ref.third }.toList())
}
}
/**
* List of threads. For instance, RTOS tasks
*/
override fun getThreads(): List<LLThread> {
return server.threads().getSafe().threads.asSequence().map(Util::threadJBFromDAP).toList()
}
override fun cancelSymbolsDownload(details: String) {
throw DebuggerCommandException("cancelSymbolsDownload not implemented")
}
/**
* Stack trace for a thread
*/
override fun getFrames(thread: LLThread, from: Int, count: Int, untilFirstLineWithCode: Boolean): ResultList<LLFrame> {
val args = StackTraceArguments()
args.threadId = Math.toIntExact(thread.id)
args.startFrame = from
args.levels = count
val stackTrace = server.stackTrace(args).getSafe()
val stackFrames = stackTrace.stackFrames
val resultList = stackFrames.map { Util.frameJBFromDAP(it, null, modules) }
return ResultList.create(resultList, false)
}
@Deprecated("Inherited from deprecated")
override fun getVariables(p0: Long, p1: Int): MutableList<LLValue> {
deprecated()
}
override fun getFrameVariables(thread: LLThread, frame: LLFrame): FrameVariables {
return FrameVariables(getWrappedScopes(frame), true)
}
// TODO registers
override fun supportsRegisters(): Boolean {
return true
}
override fun getRegisters(thread: LLThread, frame: LLFrame): List<LLValue> {
return registerSets.values
.asSequence()
.flatMap { it.asSequence() }
.toList()
}
override fun getRegisters(thread: LLThread, frame: LLFrame, registerNames: Set<String>): List<LLValue> {
if (registerNames.isEmpty()) {
return getRegisters(thread, frame)
}
return registerSets.values
.asSequence()
.flatMap { it.asSequence() }
.filter { registerNames.contains(it.name.lowercase()) }
.toList()
}
override fun getRegisterSets(): List<LLRegisterSet> {
return registerSets.entries
.asSequence()
.map { LLRegisterSet(it.key, it.value.map(LLValue::getName)) }
.toList()
}
@Throws(ExecutionException::class)
protected fun getWrappedScopes(frame: LLFrame): List<LLValue> {
val scopeArgs = ScopesArguments()
val frameID = frame.index
scopeArgs.frameId = frameID
val scopes = server.scopes(scopeArgs).getSafe()
val result = ArrayList<LLValue>()
for (scope in scopes.scopes) {
val ref = scope.variablesReference
if ("registers" == scope.name.lowercase()) {
updateRegisters(frameID, ref)
continue
}
result.addAll(getVariables(frameID, scope.variablesReference, null, null))
}
return result
}
@Throws(ExecutionException::class)
private fun updateRegisters(frameID: Int, rootRef: Int) {
val registerGroups = getVariables(frameID, rootRef, null, null)
registerSets.clear()
var c = 0
for (registerGroup in registerGroups) {
val name = "${c++} - ${registerGroup.name}"
val ref = registerGroup.getUserData(LLVALUE_CHILDREN_REF)
if (ref == null || ref == 0)
continue
val registers = getVariables(frameID, ref, null, null)
val renamedRegisters = ArrayList<LLValue>(registers.size)
for (register in registers) {
val renamedRegister = LLValue(register.name.lowercase(), register.type, register.displayType, register.address, register.typeClass, register.referenceExpression)
register.copyUserDataTo(renamedRegister)
val oldData = renamedRegister.getUserData(LLVALUE_DATA)
if (oldData != null && HEX_REGEX.matches(oldData.value)) {
val newData = LLValueData("0x${oldData.value.lowercase()}", oldData.description, oldData.hasLongerDescription(), oldData.mayHaveChildren(), oldData.isSynthetic)
renamedRegister.putUserData(LLVALUE_DATA, newData)
}
renamedRegisters.add(renamedRegister)
}
registerSets[name] = renamedRegisters
}
val arch = architecture
if (arch == null)
return
val toggles = HashMap<String, Boolean>()
var first = true
for (registerSet in registerSets.keys) {
toggles[registerSet] = first
first = false
}
val cds = CidrDebuggerSettings.getInstance()
val settings = cds.getRegisterSetSettings(arch, driverName)
if (settings == null || !settings.keys.containsAll(toggles.keys)) {
cds.setRegisterSetSettings(arch, driverName, toggles)
}
}
override fun getArchitecture(): String? {
return null
}
@Throws(ExecutionException::class)
protected fun getVariables(frameID: Int, variablesReference: Int, start: Int?, count: Int?): List<LLValue> {
val variableArgs = VariablesArguments()
variableArgs.variablesReference = variablesReference
variableArgs.start = start
variableArgs.count = count
val variables = server.variables(variableArgs).getSafe().variables
val javaVariables = ArrayList<LLValue>(variables.size)
for (variable in variables) {
val address = Util.parseAddressNullable(variable.memoryReference)
val type = variable.type ?: ""
val truncated = type.replace(ERROR_REGEX, "error{...}")
val name = variable.name
val evalName = variable.evaluateName ?: ""
val childRef = variable.variablesReference
val knownValue = variable.value
val llValue = LLValue(name, type, truncated, address, null, evalName)
llValue.putUserData(LLVALUE_FRAME, frameID)
llValue.putUserData(LLVALUE_CHILDREN_REF, childRef)
if (knownValue != null) {
llValue.putUserData(LLVALUE_DATA, LLValueData(knownValue, null, false, childRef > 0, false))
}
javaVariables.add(llValue)
}
return javaVariables
}
override fun getVariables(thread: LLThread, frame: LLFrame): MutableList<LLValue> {
return getFrameVariables(thread, frame).variables
}
/**
* Read value of a variable
*/
override fun getData(value: LLValue): LLValueData {
var result = ""
var childrenRef = 0
var failed = false
if (value.referenceExpression.isBlank()) {
failed = true
} else {
val args = EvaluateArguments()
args.context = EvaluateArgumentsContext.VARIABLES
args.expression = value.referenceExpression
args.frameId = value.getUserData(LLVALUE_FRAME)
val res = server.evaluate(args).getSafe()
childrenRef = res.variablesReference
if (childrenRef > 0) {
value.putUserData(LLVALUE_CHILDREN_REF, childrenRef)
}
val attribs = res.presentationHint?.attributes
if (attribs != null) {
for (attrib in attribs) {
if ("failedEvaluation" == attrib) {
failed = true
}
}
}
result = res.result
}
if (failed) {
val known = value.getUserData(LLVALUE_DATA)
if (known != null)
return known
val cRef = value.getUserData(LLVALUE_CHILDREN_REF)
if (cRef != null)
childrenRef = cRef
}
return LLValueData(result, null, false, childrenRef > 0, false)
}
/**
* Read description of a variable
*/
override fun getDescription(value: LLValue, maxLength: Int): String {
val type = value.type
val length = min(type.length, maxLength)
return type.substring(0, length)
}
/**
* Unions, structures, or classes are hierarchical. This method help to obtain the hierarchy
*/
override fun getChildrenCount(value: LLValue): Int {
val frame = value.getUserData(LLVALUE_FRAME)
val childrenRef = value.getUserData(LLVALUE_CHILDREN_REF)
val children = if (childrenRef == null || frame == null)
emptyList()
else
getVariables(frame, childrenRef, null, null)
value.putUserData(LLVALUE_CHILDREN, children)
return children.size
}
/**
* Unions, structures, or classes are hierarchical. This method help to obtain the hierarchy
*/
override fun getVariableChildren(value: LLValue, from: Int, count: Int): ResultList<LLValue> {
val size = getChildrenCount(value)
val children = value.getUserData(LLVALUE_CHILDREN)
return if (children == null || from > size) {
ResultList.empty()
} else if (from + count >= size) {
ResultList.create(children.subList(from, size), false)
} else {
ResultList.create(children.subList(from, from + count), true)
}
}
@Deprecated("Inherited from deprecated")
override fun evaluate(threadId: Long, frameIndex: Int, expression: String, language: DebuggerLanguage?): LLValue {
val evalArgs = EvaluateArguments()
evalArgs.frameId = frameIndex
evalArgs.expression = expression
val res = server.evaluate(evalArgs).getSafe()
val type = res.type ?: "unknown"
val addr = res.memoryReference?.let { Util.parseAddress(it) }
val result = LLValue("result", type, addr, null, "")
result.putUserData(LLVALUE_DATA, LLValueData(res.result, null, false, false, false))
result.putUserData(LLVALUE_FRAME, frameIndex)
return result
}
@Suppress("DEPRECATION")
override fun evaluate(thread: LLThread, frame: LLFrame, expression: String, language: DebuggerLanguage?): LLValue {
return evaluate(thread.id, frame.index, expression, language)
}
override fun disassembleFunction(address: Address, fallbackRange: AddressRange): List<LLInstruction> {
return disassemble(fallbackRange)
}
override fun disassemble(range: AddressRange): List<LLInstruction> {
return runBlocking {
disassembleSuspend(range)
}
}
private suspend fun disassembleSuspend(range: AddressRange): List<LLInstruction> {
if (!capabilities.supportsDisassembleRequest)
throw DebuggerCommandException("disassemble is not supported by debugger!")
val args = DisassembleArguments()
args.memoryReference = java.lang.Long.toHexString(range.start.unsignedLongValue)
args.instructionCount = Math.toIntExact(range.size)
args.resolveSymbols = true
val disassembly = server.disassemble(args).getSuspend()
val dapInstructions = disassembly.instructions
val jbInstructions = ArrayList<LLInstruction>(dapInstructions.size)
var loc: Source? = null
var startLine: Int? = null
var endLine: Int? = null
var symbol: String? = null
var baseOffset = 0L
for (dapInstruction in dapInstructions) {
val dapLoc = dapInstruction.location
val dapStartLine = dapInstruction.line
val dapEndLine = dapInstruction.endLine
val dapSymbol = dapInstruction.symbol
val dapAddr = Util.parseAddress(dapInstruction.address)
var uniq = true
if (dapLoc != null) {
loc = dapLoc
} else if (startLine != null && dapStartLine == startLine && endLine != null && dapEndLine == endLine) {
uniq = false
} else {
startLine = dapStartLine
endLine = dapEndLine
}
if (dapSymbol != null && dapSymbol != symbol) {
symbol = dapSymbol
baseOffset = dapAddr
}
val llSymbol = symbol?.let {LLSymbolOffset(symbol, dapAddr - baseOffset)}
jbInstructions.add(Util.instructionJBFromDAP(dapInstruction, loc, startLine, endLine, uniq, llSymbol))
}
return jbInstructions
}
override fun dumpMemory(range: AddressRange): List<LLMemoryHunk> {
if (!capabilities.supportsReadMemoryRequest)
throw DebuggerCommandException("dumpMemory is not supported by debugger!")
val start = range.start.unsignedLongValue
val length = range.size
val hunks = ArrayList<LLMemoryHunk>((length / (Integer.MAX_VALUE.toLong() + 1)).toInt())
var offset = 0L
while (offset < length) {
val blockLength = Math.toIntExact(min(Integer.MAX_VALUE.toLong(), length - offset))
val args = ReadMemoryArguments()
args.memoryReference = Util.stringifyAddress(start + offset)
args.count = blockLength
hunks.add(Util.memoryJBFromDAP(server.readMemory(args).getSafe()))
offset += Integer.MAX_VALUE
}
return hunks
}
override fun getLoadedModules(): List<LLModule> {
if (!capabilities.supportsModulesRequest)
throw DebuggerCommandException("getLoadedModules is not supported by debugger!")
val args = ModulesArguments()
val modulesResponse = server.modules(args).getSafe()
val modules = modulesResponse.modules
return modules.map(Util::moduleJBFromDAP)
}
override fun getModuleSections(module: LLModule): List<LLSection> {
throw DebuggerCommandException("GetModuleSections is not implemented")
}
override fun executeShellCommand(executable: String, params: List<String>?, workingDir: String?, timeoutSecs: Int): ShellCommandResult {
throw ExecutionException("ExecuteShellCommand is not implemented")
}
override fun executeInterpreterCommand(command: String): String {
return executeInterpreterCommand(-1, -1, command)
}
override fun executeInterpreterCommand(threadId: Long, frameIndex: Int, text: String): String {
val args = EvaluateArguments()
args.expression = text
args.frameId = frameIndex
return server.evaluate(args).getSafe().result
}
override fun handleSignal(signalName: String, stop: Boolean, pass: Boolean, notify: Boolean) {
throw DebuggerCommandException("handleSignal is not implemented")
}
override fun getPromptText(): String {
return ""
}
/**
* Verify if driver is in OK state
*
* @throws ExecutionException if something is wrong
*/
override fun checkErrors() {
// TODO
}
override fun addSymbolsFile(file: File, module: File?) {
throw ExecutionException("addSymbolsFile not implemented!")
}
override fun getProcessInput(): OutputStream? {
return multiplexer
}
override fun getDisasmFlavor(): DisasmFlavor {
return DisasmFlavor.INTEL
}
companion object {
val LLVALUE_FRAME = Key.create<Int>("DAPDriver.LLVALUE_FRAME")
val LLVALUE_CHILDREN_REF = KeyWithDefaultValue.create<Int>("DAPDriver.LLVALUE_CHILDREN_REF", 0)
val LLVALUE_DATA = Key.create<LLValueData>("DAPDriver.LLVALUE_DATA")
val LLVALUE_CHILDREN = Key.create<List<LLValue>>("DAPDriver.LLVALUE_CHILDREN")
}
data class PathedSourceBreakpoint(
val path: String,
val src: SourceBreakpoint
)
data class MappedBreakpoint(
val id: Int,
val java: LLBreakpoint,
val loc: LLBreakpointLocation?,
val dap: Breakpoint,
val ref: Either3<PathedSourceBreakpoint, FunctionBreakpoint, InstructionBreakpoint>
) {
constructor(dap: Breakpoint, ref: Either3<PathedSourceBreakpoint, FunctionBreakpoint, InstructionBreakpoint>) :
this(dap.id, Util.breakpointJBFromDAP(dap), Util.getLocation(dap), dap, ref)
}
data class MappedModule(
val java: LLModule,
val dap: Module
) {
constructor(dap: Module): this(Util.moduleJBFromDAP(dap), dap)
}
abstract inner class DAPDebuggerClient: IDebugProtocolClient {
override fun runInTerminal(args: RunInTerminalRequestArguments): CompletableFuture<RunInTerminalResponse> {
return zigCoroutineScope.async {
runInTerminalAsync(args)
}.asCompletableFuture()
}
fun runInTerminalAsync(args: RunInTerminalRequestArguments): RunInTerminalResponse {
val cli = PtyCommandLine(args.args.toList())
cli.charset = Charsets.UTF_8
val cwd = args.cwd?.ifBlank { null }?.toNioPathOrNull()
if (cwd != null) {
cli.withWorkingDirectory(cwd)
}
val childProcess = ZigProcessHandler(cli)
this@DAPDriver.childProcess = childProcess
childProcess.addProcessListener(object: ProcessListener {
override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) {
handleTargetOutput(
event.text,
if (ProcessOutputType.isStdout(outputType)) {
ProcessOutputType.STDOUT
} else if (ProcessOutputType.isStderr(outputType)) {
ProcessOutputType.STDERR
} else {
ProcessOutputType.SYSTEM
}
)
}
})
childProcess.startNotify()
val processInput = childProcess.processInput
this@DAPDriver.processInput = processInput
val resp = RunInTerminalResponse()
resp.shellProcessId = childProcess.process.pid().toInt()
zigCoroutineScope.launch {
dummyOutput?.toByteArray()?.let { processInput.write(it) }
dummyOutput = null
}
return resp
}
override fun output(args: OutputEventArguments) {
when(args.category) {
"stdout" -> handleTargetOutput(args.output, ProcessOutputType.STDOUT)
"stderr" -> handleTargetOutput(args.output, ProcessOutputType.STDERR)
else -> handleDebuggerOutput(args.output, ProcessOutputType.STDOUT)
}
}
override fun breakpoint(args: BreakpointEventArguments) {
val bp = args.breakpoint
when(args.reason) {
BreakpointEventArgumentsReason.CHANGED -> {
val mbp = updateBP(bp)
handleBreakpointUpdated(mbp.java)
handleBreakpointLocationsReplaced(mbp.id, listOfNotNull(mbp.loc))
}
BreakpointEventArgumentsReason.NEW -> {
val mbp = updateBP(bp)
handleBreakpointAdded(mbp.java)
handleBreakpointLocationsReplaced(mbp.id, listOfNotNull(mbp.loc))
}
BreakpointEventArgumentsReason.REMOVED -> {
breakpoints.remove(bp.id)
handleBreakpointRemoved(bp.id)
}
}
}
private fun updateBP(bp: Breakpoint): MappedBreakpoint {
return breakpoints.compute(bp.id, { _, mbp ->
if (mbp != null)
return@compute MappedBreakpoint(bp, mbp.ref)
val ins = InstructionBreakpoint()
ins.instructionReference = bp.instructionReference
return@compute MappedBreakpoint(bp, Either3.forThird(ins))
})!!
}
override fun exited(args: ExitedEventArguments) {
handleExited(args.exitCode)
}
override fun stopped(args: StoppedEventArguments) {
server.threads().thenAccept { threadsResponse ->
val threads = threadsResponse.threads
val thread = if (args.threadId != null) {
val id = args.threadId!!
threads.asSequence().filter { it.id == id }.first()
} else {
threads.asSequence().sortedBy { it.id }.first()
}
val jbThread = Util.threadJBFromDAP(thread)
val stArgs = StackTraceArguments()
stArgs.threadId = thread.id
stArgs.startFrame = 0
stArgs.levels = 1
server.stackTrace(stArgs).thenAccept { st ->
var helperBreakpoint: MappedBreakpoint? = null
val isBreakpoint = "breakpoint" == args.reason
if (isBreakpoint) {
helperBreakpoint = breakpoints.get(args.hitBreakpointIds[0])
}
val frame = Util.frameJBFromDAP(st.stackFrames[0], helperBreakpoint, modules)
val stopPlace = StopPlace(jbThread, frame)
if (isBreakpoint) {
handleBreakpoint(stopPlace, args.hitBreakpointIds[0])
} else {
handleInterrupted(stopPlace)
}
}
}
}
override fun continued(args: ContinuedEventArguments) {
handleRunning()
}
override fun module(args: ModuleEventArguments) {
val module = args.module
when(args.reason) {
ModuleEventArgumentsReason.NEW -> {
val mm = MappedModule(module)
modules[module.id.left] = mm
handleModulesLoaded(listOf(mm.java))
}
ModuleEventArgumentsReason.CHANGED -> {
val newModule = MappedModule(module)
val oldModule = modules.put(module.id.left, newModule)
if (oldModule != null) {
handleModulesUnloaded(listOf(oldModule.java))
}
handleModulesLoaded(listOf(newModule.java))
}
ModuleEventArgumentsReason.REMOVED -> {
val oldModule = modules.remove(module.id.left)
if (oldModule != null) {
handleModulesUnloaded(listOf(oldModule.java))
}
}
null -> {}
}
}
}
}
@Throws(ExecutionException::class)
fun <T> CompletableFuture<T>.getSafe(): T {
private fun <T> CompletableFuture<T>.getSafe(): T {
try {
return getMaybeCancellable()
} catch (e: Exception) {
@ -461,6 +1102,14 @@ fun <T> CompletableFuture<T>.getSafe(): T {
}
}
private suspend fun <T> CompletableFuture<T>.getSuspend(): T {
try {
return asDeferred().await()
} catch (e: Exception) {
throw ExecutionException(e)
}
}
private fun notSupported(): Nothing {
throw ExecutionException("Not supported")
}
@ -468,3 +1117,6 @@ private fun notSupported(): Nothing {
private fun deprecated(): Nothing {
throw ExecutionException("Deprecated")
}
private val HEX_REGEX = Regex("[0-9a-fA-F]+")
private val ERROR_REGEX = Regex("error\\{.*?}")

View file

@ -49,7 +49,7 @@ object Util {
var sourcePath = if (source == null) "" else Objects.requireNonNullElseGet(
source.path
) { Objects.requireNonNullElse(source.origin, "unknown") }
sourcePath = toJBPath(sourcePath)!!
sourcePath = toJBPath(sourcePath)
return LLBreakpoint(
DAPBreakpoint.id,
sourcePath,
@ -176,21 +176,20 @@ object Util {
suspend fun instructionJBFromDAP(
DAPInstruction: DisassembledInstruction,
loc: Source?,
startLine: Int?,
endLine: Int?,
startLineIn: Int?,
endLineIn: Int?,
uniq: Boolean,
symbol: LLSymbolOffset?
): LLInstruction {
var startLine = startLine
var endLine = endLine
var startLine = startLineIn
var endLine = endLineIn
val address: Address = Address.parseHexString(DAPInstruction.address)
val byteStrings =
DAPInstruction.instructionBytes.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val bytes = ArrayList<Byte>(byteStrings.size)
for (byteString in byteStrings) {
bytes.add(byteString.toInt(16) as Byte)
bytes.add(byteString.toInt(16).toByte())
}
val result: ArrayList<LLInstruction> = ArrayList<LLInstruction>()
var comment: String? = null
if (loc != null && startLine != null && endLine != null && uniq) run {
val pathStr = toJBPath(loc.path)
@ -254,8 +253,4 @@ object Util {
throw com.intellij.execution.ExecutionException(e.cause)
}
}
fun emptyIfNull(str: String?): String {
return str ?: ""
}
}

View file

@ -0,0 +1,29 @@
/*
* This file is part of ZigBrains.
*
* Copyright (C) 2023-2024 FalsePattern
* All Rights Reserved
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* ZigBrains is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, only version 3 of the License.
*
* ZigBrains is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/
package com.falsepattern.zigbrains.debugger.settings
enum class MSVCDownloadPermission {
AskMe,
Allow,
Deny
}

View file

@ -35,8 +35,9 @@ class ZigDebuggerSettings: XDebuggerSettings<ZigDebuggerSettings>("Zig") {
var downloadAutomatically = false
var useClion = true
var msvcConsent = MSVCDownloadPermission.AskMe
override fun getState(): ZigDebuggerSettings? {
override fun getState(): ZigDebuggerSettings {
return this
}

View file

@ -23,7 +23,10 @@
package com.falsepattern.zigbrains.debugger.toolchain
import com.falsepattern.zigbrains.debugger.ZigDebugBundle
import com.falsepattern.zigbrains.debugger.settings.MSVCDownloadPermission
import com.falsepattern.zigbrains.debugger.settings.ZigDebuggerSettings
import com.falsepattern.zigbrains.debugger.toolchain.ZigDebuggerToolchainService.Companion.downloadPath
import com.falsepattern.zigbrains.shared.coroutine.withCurrentEDTModalityContext
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
@ -48,7 +51,10 @@ suspend fun msvcMetadata(): Properties {
cache?.let { return it }
mutex.withLock {
cache?.let { return it }
val allowDownload = withEDTContext(ModalityState.current()) {
val settings = ZigDebuggerSettings.instance
var permission = settings.msvcConsent
if (permission == MSVCDownloadPermission.AskMe) {
val allowDownload = withCurrentEDTModalityContext {
val dialog = DialogBuilder()
dialog.setTitle(ZigDebugBundle.message("msvc.consent.title"))
dialog.addCancelAction().setText(ZigDebugBundle.message("msvc.consent.deny"))
@ -62,7 +68,10 @@ suspend fun msvcMetadata(): Properties {
dialog.centerPanel(centerPanel)
dialog.showAndGet()
}
val data = if (allowDownload) {
permission = if (allowDownload) MSVCDownloadPermission.Allow else MSVCDownloadPermission.Deny
settings.msvcConsent = permission
}
val data = if (permission == MSVCDownloadPermission.Allow) {
withTimeoutOrNull(3000L) {
downloadMSVCProps()
} ?: run {

View file

@ -157,7 +157,7 @@ class ZigDebuggerToolchainService {
val centerPanel = JBPanel<JBPanel<*>>()
val hyperlink = HyperlinkLabel()
hyperlink.setTextWithHyperlink(msvcUrl.dialogBody)
hyperlink.setHyperlinkText(msvcUrl.dialogLink)
hyperlink.setHyperlinkTarget(msvcUrl.dialogLink)
hyperlink.addHyperlinkListener(BrowserHyperlinkListener())
centerPanel.add(hyperlink)
dialog.centerPanel(centerPanel)
@ -229,7 +229,7 @@ class ZigDebuggerToolchainService {
val binaryToDownload = binariesToDownload.first { it.url == downloadUrl }
val propertyName = binaryToDownload.propertyName
val archiveFile = result.first
Unarchiver.unarchive(archiveFile, downloadDir)
Unarchiver.unarchive(archiveFile, downloadDir, binaryToDownload.prefix)
archiveFile.delete()
versions[propertyName] = binaryToDownload.version
}

View file

@ -34,14 +34,14 @@ abstract class MSVCDriverConfiguration: DAPDebuggerDriverConfiguration() {
protected abstract val debuggerExecutable: Path
override fun createDriver(handler: DebuggerDriver.Handler, arch: ArchitectureType): DebuggerDriver {
TODO("Not yet implemented")
return WinDAPDriver(handler).also { it.initialize(this) }
}
override fun createDriverCommandLine(driver: DebuggerDriver, arch: ArchitectureType): GeneralCommandLine {
val path = debuggerExecutable
val cli = GeneralCommandLine()
cli.exePath = path.pathString
cli.addParameters("--interpreter=vscode", "--extconfigdir=%USERPROFILE\\.cppvsdbg\\extensions")
cli.addParameters("--interpreter=vscode", "--extconfigdir=%USERPROFILE%\\.cppvsdbg\\extensions")
cli.withWorkingDirectory(path.parent)
return cli
}

View file

@ -0,0 +1,129 @@
/*
* This file is part of ZigBrains.
*
* Copyright (C) 2023-2024 FalsePattern
* All Rights Reserved
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* ZigBrains is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, only version 3 of the License.
*
* ZigBrains is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/
package com.falsepattern.zigbrains.debugger.win
import com.falsepattern.zigbrains.debugger.dap.DAPDriver
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.util.system.CpuArch
import com.jetbrains.cidr.ArchitectureType
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.sync.Semaphore
import org.eclipse.lsp4j.debug.Capabilities
import org.eclipse.lsp4j.debug.OutputEventArguments
import org.eclipse.lsp4j.debug.services.IDebugProtocolServer
import org.eclipse.lsp4j.debug.util.ToStringBuilder
import org.eclipse.lsp4j.jsonrpc.MessageConsumer
import org.eclipse.lsp4j.jsonrpc.debug.messages.DebugResponseMessage
import org.eclipse.lsp4j.jsonrpc.messages.Message
import org.eclipse.lsp4j.jsonrpc.services.JsonRequest
import java.lang.RuntimeException
import java.security.MessageDigest
import java.util.Base64
import java.util.concurrent.CompletableFuture
import java.util.zip.Inflater
class WinDAPDriver(handler: Handler) : DAPDriver<IDebugProtocolServer, WinDAPDriver.WinDAPDebuggerClient>(handler) {
private val handshakeFinished = Semaphore(1, 1)
override fun createDebuggerClient(): WinDAPDebuggerClient {
return WinDAPDebuggerClient()
}
override fun getServerInterface(): Class<IDebugProtocolServer> {
return IDebugProtocolServer::class.java
}
override fun wrapMessageConsumer(mc: MessageConsumer): MessageConsumer {
return object: MessageConsumer {
private var verifyHandshake = true
override fun consume(message: Message) {
if (verifyHandshake && message is DebugResponseMessage && message.method == "handshake") {
verifyHandshake = false
message.setResponseId(1)
}
mc.consume(message)
}
}
}
override suspend fun postInitialize(capabilities: Capabilities) {
handshakeFinished.acquire()
}
inner class WinDAPDebuggerClient: DAPDriver<IDebugProtocolServer, WinDAPDriver.WinDAPDebuggerClient>.DAPDebuggerClient() {
override fun output(args: OutputEventArguments) {
if ("telemetry" == args.category)
return
super.output(args)
}
@JsonRequest
fun handshake(handshake: HandshakeRequest): CompletableFuture<HandshakeResponse> {
return zigCoroutineScope.async(Dispatchers.IO) {
handshakeSuspend(handshake)
}.asCompletableFuture()
}
private fun handshakeSuspend(handshake: HandshakeRequest): HandshakeResponse {
val hasher = MessageDigest.getInstance("SHA-256")
hasher.update(handshake.value.encodeToByteArray())
val inflater = Inflater(true)
val coconut = DAPDebuggerClient::class.java.getResourceAsStream("/coconut.jpg").use { it.readAllBytes() } ?: throw RuntimeException("No coconut")
inflater.setInput(coconut, coconut.size - 80, 77)
inflater.finished()
val b = ByteArray(1)
while (inflater.inflate(b) > 0)
hasher.update(b)
val result = HandshakeResponse(String(coconut, coconut.size - 3, 3) + Base64.getEncoder().encodeToString(hasher.digest()))
handshakeFinished.release()
return result
}
}
override fun getArchitecture(): String {
return ArchitectureType.forVmCpuArch(CpuArch.CURRENT).id
}
data class HandshakeRequest(var value: String) {
constructor() : this("")
override fun toString(): String {
val b = ToStringBuilder(this)
b.add("value", value)
return b.toString()
}
}
data class HandshakeResponse(var signature: String) {
constructor() : this("")
override fun toString(): String {
val b = ToStringBuilder(this)
b.add("signature", this.signature)
return b.toString()
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View file

@ -33,6 +33,8 @@ import com.intellij.execution.configurations.RunnerSettings
import com.intellij.execution.runners.AsyncProgramRunner
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.execution.ui.RunContentDescriptor
import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.rd.util.toPromise
import com.intellij.platform.ide.progress.ModalTaskOwner
@ -59,7 +61,14 @@ abstract class ZigProgramRunner<ProfileState: ZigProfileState<*>>(protected val
val state = castProfileState(baseState) ?: return null
val toolchain = environment.project.zigProjectSettings.state.toolchain ?: return null
val toolchain = environment.project.zigProjectSettings.state.toolchain ?: run {
Notification(
"zigbrains",
"Zig project toolchain not set, cannot execute program!",
NotificationType.ERROR
).notify(environment.project)
return null
}
return reportProgress { reporter ->
reporter.indeterminateStep("Saving all documents") {

View file

@ -51,6 +51,8 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import javax.swing.event.DocumentEvent
import kotlin.io.path.exists
import kotlin.io.path.notExists
import kotlin.io.path.pathString
class ZigProjectSettingsPanel(private val project: Project?) : Disposable {
@ -100,8 +102,8 @@ class ZigProjectSettingsPanel(private val project: Project?) : Disposable {
get() = ZigProjectSettings(
direnv.isSelected,
stdFieldOverride.isSelected,
pathToStd.text,
pathToToolchain.text
pathToStd.text.ifBlank { null },
pathToToolchain.text.ifBlank { null }
)
set(value) {
direnv.isSelected = value.direnv
@ -148,22 +150,28 @@ class ZigProjectSettingsPanel(private val project: Project?) : Disposable {
private suspend fun updateUI() {
val pathToToolchain = this.pathToToolchain.text.toNioPathOrNull()
val pathToToolchain = this.pathToToolchain.text.ifBlank { null }?.toNioPathOrNull()
delay(200)
val toolchain = pathToToolchain?.let { LocalZigToolchain(it) }
val zig = toolchain?.zig
val env = zig?.getEnv(project)
val version = env?.version
val stdPath = env?.stdPath(toolchain)
withEDTContext {
toolchainVersion.text = version ?: ""
if (zig?.path()?.exists() != true) {
toolchainVersion.text = ""
if (!stdFieldOverride.isSelected) {
pathToStd.text = ""
}
return
}
val env = zig.getEnv(project)
val version = env.version
val stdPath = env.stdPath(toolchain, project)
toolchainVersion.text = version
toolchainVersion.foreground = JBColor.foreground()
if (!stdFieldOverride.isSelected) {
pathToStd.text = stdPath?.pathString ?: ""
}
}
}
override fun dispose() {
debounce?.cancel("Disposed")

View file

@ -21,6 +21,7 @@
*/
package com.falsepattern.zigbrains.project.toolchain
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.io.toNioPathOrNull
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@ -37,12 +38,12 @@ data class ZigToolchainEnvironmentSerializable(
@SerialName("version") val version: String,
@SerialName("target") val target: String
) {
fun stdPath(toolchain: LocalZigToolchain): Path? {
fun stdPath(toolchain: LocalZigToolchain, project: Project?): Path? {
val path = stdDirectory.toNioPathOrNull() ?: return null
if (path.isAbsolute)
return path
val resolvedPath = toolchain.location.resolve(path)
val resolvedPath = toolchain.workingDirectory(project)?.resolve(path) ?: return null
if (resolvedPath.isAbsolute)
return resolvedPath

View file

@ -103,7 +103,7 @@ private suspend fun getRoots(
}
}
if (toolchain != null) {
val stdPath = toolchain.zig.getEnv(project).stdPath(toolchain) ?: return emptySet()
val stdPath = toolchain.zig.getEnv(project).stdPath(toolchain, project) ?: return emptySet()
val roots = stdPath.refreshAndFindVirtualDirectory() ?: return emptySet()
return setOf(roots)
}

View file

@ -54,6 +54,12 @@ suspend inline fun <T> withEDTContext(state: ModalityState = ModalityState.defau
return withContext(Dispatchers.EDT + state.asContextElement(), block = block)
}
suspend inline fun <T> withCurrentEDTModalityContext(noinline block: suspend CoroutineScope.() -> T): T {
return withContext(Dispatchers.EDT + ModalityState.defaultModalityState().asContextElement()) {
withContext(Dispatchers.EDT + ModalityState.current().asContextElement(), block = block)
}
}
suspend inline fun <T> runInterruptibleEDT(state: ModalityState = ModalityState.defaultModalityState(), noinline targetAction: () -> T): T {
return runInterruptible(Dispatchers.EDT + state.asContextElement(), block = targetAction)
}