Skip to content

Commit

Permalink
(#19) Debugger: refactor and update the connection code to support TC…
Browse files Browse the repository at this point in the history
…P for debugger
  • Loading branch information
ForNeVeR committed Aug 16, 2024
1 parent bf5bcc7 commit 7891c1f
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 136 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import com.intellij.openapi.application.EDT
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.rd.util.toPromise
import com.intellij.openapi.rd.util.withUiContext
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.plugin.powershell.ide.MessagesBundle
import com.intellij.plugin.powershell.ide.PluginProjectRoot
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import com.intellij.plugin.powershell.lang.lsp.languagehost.PSLanguageHostUtils.
import com.intellij.plugin.powershell.lang.lsp.languagehost.PSLanguageHostUtils.getEditorServicesModuleVersion
import com.intellij.plugin.powershell.lang.lsp.languagehost.PSLanguageHostUtils.getEditorServicesStartupScript
import com.intellij.plugin.powershell.lang.lsp.languagehost.PSLanguageHostUtils.getPSExtensionModulesDir
import com.intellij.util.application
import com.intellij.util.execution.ParametersListUtil
import com.intellij.util.io.BaseOutputReader
import com.intellij.util.io.await
Expand All @@ -35,7 +34,6 @@ import com.sun.jna.platform.win32.Kernel32
import com.sun.jna.platform.win32.WinNT
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import org.jetbrains.plugins.terminal.TerminalProjectOptionsProvider
import kotlinx.coroutines.withContext
import org.newsclub.net.unix.AFUNIXSocket
import org.newsclub.net.unix.AFUNIXSocketAddress
Expand Down Expand Up @@ -74,35 +72,19 @@ open class EditorServicesLanguageHostStarter(protected val myProject: Project) :
"${ApplicationInfo.getInstance().majorVersion}.${ApplicationInfo.getInstance().minorVersion}"

private data class HostDetails(val name: String, val profileId: String, val version: String)
open class SessionInfo private constructor(val powerShellVersion: String?, val status: String?) {

class Pipes(
val languageServiceReadPipeName: String,
val languageServiceWritePipeName: String,
val debugServiceReadPipeName: String,
val debugServiceWritePipeName: String,
powerShellVersion: String?,
status: String?
) : SessionInfo(powerShellVersion, status) {
override fun toString(): String {
return "{languageServiceReadPipeName:$languageServiceReadPipeName,languageServiceWritePipeName:$languageServiceWritePipeName," +
"debugServiceReadPipeName:$debugServiceReadPipeName,debugServiceWritePipeName:$debugServiceWritePipeName," +
"powerShellVersion:$powerShellVersion,status:$status}"
}
}

class Tcp(
val languageServicePort: Int,
val debugServicePort: Int,
powerShellVersion: String?,
status: String?
) : SessionInfo(powerShellVersion, status) {
override fun toString(): String {
return "{languageServicePort:$languageServicePort,debugServicePort:$debugServicePort,powerShellVersion:$powerShellVersion,status:$status}"
}
}
sealed class ConnectionInfo {
data class Pipes(val readPipeName: String, val writePipeName: String) : ConnectionInfo()
data class Tcp(val port: Int) : ConnectionInfo()
}

data class SessionInfo(
val powerShellVersion: String?,
val status: String?,
val languageService: ConnectionInfo?,
val debuggerService: ConnectionInfo?
)

/**
* @throws PowerShellExtensionNotFound
*/
Expand Down Expand Up @@ -133,99 +115,65 @@ open class EditorServicesLanguageHostStarter(protected val myProject: Project) :
}
}

/**
* @throws PowerShellExtensionError
* @throws PowerShellExtensionNotFound
* @throws PowerShellNotInstalled
*/
override suspend fun establishConnection(): Pair<InputStream?, OutputStream?> {
val sessionInfo = startServerSession() ?: return Pair(null, null)
if (sessionInfo is SessionInfo.Pipes) {
val readPipeName = sessionInfo.languageServiceReadPipeName
val writePipeName = sessionInfo.languageServiceWritePipeName
return withContext(Dispatchers.IO) {
if (SystemInfo.isWindows) {
val readPipe = RandomAccessFile(readPipeName, "rwd")
val writePipe = RandomAccessFile(writePipeName, "r")
val serverReadChannel = readPipe.channel
val serverWriteChannel = writePipe.channel
val inSf = Channels.newInputStream(serverWriteChannel)
val outSf = BufferedOutputStream(Channels.newOutputStream(serverReadChannel))
Pair(inSf, outSf)
} else {
val readSock = AFUNIXSocket.newInstance()
val writeSock = AFUNIXSocket.newInstance()
readSock.connect(AFUNIXSocketAddress.of(File(readPipeName)))
writeSock.connect(AFUNIXSocketAddress.of(File(writePipeName)))
Pair(writeSock.inputStream, readSock.outputStream)
private suspend fun establishConnection(connectionInfo: ConnectionInfo): Pair<InputStream, OutputStream>? =
when (connectionInfo) {
is ConnectionInfo.Pipes -> {
val readPipeName = connectionInfo.readPipeName
val writePipeName = connectionInfo.writePipeName
withContext(Dispatchers.IO) {
if (SystemInfo.isWindows) {
val readPipe = RandomAccessFile(readPipeName, "rwd")
val writePipe = RandomAccessFile(writePipeName, "r")
val serverReadChannel = readPipe.channel
val serverWriteChannel = writePipe.channel
val inSf = Channels.newInputStream(serverWriteChannel)
val outSf = BufferedOutputStream(Channels.newOutputStream(serverReadChannel))
Pair(inSf, outSf)
} else {
val readSock = AFUNIXSocket.newInstance()
val writeSock = AFUNIXSocket.newInstance()
readSock.connect(AFUNIXSocketAddress.of(File(readPipeName)))
writeSock.connect(AFUNIXSocketAddress.of(File(writePipeName)))
Pair(writeSock.inputStream, readSock.outputStream)
}
}
}
} else {
return withContext(Dispatchers.IO) block@{
val port = (sessionInfo as? SessionInfo.Tcp)?.languageServicePort ?: return@block Pair(null, null)
try {
socket = Socket("127.0.0.1", port)
} catch (e: Exception) {
logger.error("Unable to open connection to language host: $e")
}
if (socket == null) {
logger.error("Unable to create socket: " + toString())
}
if (socket?.isConnected == true) {
logger.info("Connection to language host established: ${socket?.localPort} -> ${socket?.port}")
val inputStream = socket?.getInputStream()
val outputStream = socket?.getOutputStream()
if (inputStream != null && outputStream != null) return@block Pair(inputStream, outputStream)
}
is ConnectionInfo.Tcp -> {
withContext(Dispatchers.IO) block@{
val port = connectionInfo.port
try {
socket = Socket("127.0.0.1", port)
} catch (e: Exception) {
logger.error("Unable to open connection to language host: $e")
}
if (socket == null) {
logger.error("Unable to create socket: " + toString())
}
if (socket?.isConnected == true) {
logger.info("Connection to language host established: ${socket?.localPort} -> ${socket?.port}")
val inputStream = socket?.getInputStream()
val outputStream = socket?.getOutputStream()
if (inputStream != null && outputStream != null) Pair(inputStream, outputStream)
}

Pair(null, null)
null
}
}
}

/**
* @throws PowerShellExtensionError
* @throws PowerShellExtensionNotFound
* @throws PowerShellNotInstalled
*/
override suspend fun establishConnection(): Pair<InputStream, OutputStream>? {
val sessionInfo = startServerSession() ?: return null
return sessionInfo.languageService?.let { establishConnection(it) }
}

override suspend fun establishDebuggerConnection(): Pair<InputStream, OutputStream>? {
val sessionInfo = startServerSession(true) ?: return null
if (sessionInfo is SessionInfo.Pipes) {
val readPipeName = sessionInfo.debugServiceReadPipeName
val writePipeName = sessionInfo.debugServiceWritePipeName
return withContext(Dispatchers.IO) {
if (SystemInfo.isWindows) {
val readPipe = RandomAccessFile(readPipeName, "rwd")
val writePipe = RandomAccessFile(writePipeName, "r")
val serverReadChannel = readPipe.channel
val serverWriteChannel = writePipe.channel
val inSf = Channels.newInputStream(serverWriteChannel)
val outSf = BufferedOutputStream(Channels.newOutputStream(serverReadChannel))
Pair(inSf, outSf)
} else {
val readSock = AFUNIXSocket.newInstance()
val writeSock = AFUNIXSocket.newInstance()
readSock.connect(AFUNIXSocketAddress.of(File(readPipeName)))
writeSock.connect(AFUNIXSocketAddress.of(File(writePipeName)))
Pair(writeSock.inputStream, readSock.outputStream)
}
}
} else {
return withContext(Dispatchers.IO) block@{
val port = (sessionInfo as? SessionInfo.Tcp)?.debugServicePort ?: return@block null
try {
socket = Socket("127.0.0.1", port)
} catch (e: Exception) {
logger.error("Unable to open connection to language host: $e")
}
if (socket == null) {
logger.error("Unable to create socket: " + toString())
}
if (socket?.isConnected == true) {
logger.info("Connection to language host established: ${socket?.localPort} -> ${socket?.port}")
val inputStream = socket?.getInputStream()
val outputStream = socket?.getOutputStream()
if (inputStream != null && outputStream != null) return@block Pair(inputStream, outputStream)
}

null
}
}
val sessionInfo = startServerSession(isDebugServiceOnly = true) ?: return null
return sessionInfo.debuggerService?.let { establishConnection(it) }
}

private fun getSessionCount(): Int {
Expand Down Expand Up @@ -322,7 +270,7 @@ open class EditorServicesLanguageHostStarter(protected val myProject: Project) :
* @throws PowerShellExtensionNotFound
* @throws PowerShellNotInstalled
*/
public suspend fun startServerSession(isDebugServiceOnly: Boolean = false): SessionInfo? {
suspend fun startServerSession(isDebugServiceOnly: Boolean = false): SessionInfo? {
cachedPowerShellExtensionDir = null
cachedEditorServicesModuleVersion = null
val commandLine = buildCommandLine(isDebugServiceOnly)
Expand Down Expand Up @@ -398,35 +346,40 @@ open class EditorServicesLanguageHostStarter(protected val myProject: Project) :
handler.startNotify()
}

private fun readTcpInfo(jsonResult: JsonObject): SessionInfo? {
val langServicePort = jsonResult.get("languageServicePort")?.asInt
val debugServicePort = jsonResult.get("debugServicePort")?.asInt
val powerShellVersion = jsonResult.get("powerShellVersion")?.asString
val status = jsonResult.get("status")?.asString
if (langServicePort == null || debugServicePort == null) {
logger.warn("languageServicePort or debugServicePort are null")
private fun readTcpConnectionInfo(prefix: String, jsonResult: JsonObject): ConnectionInfo.Tcp? {
val port = jsonResult.get("${prefix}Port")?.asInt
if (port == null) {
return null
}
return SessionInfo.Tcp(langServicePort, debugServicePort, powerShellVersion, status)
return ConnectionInfo.Tcp(port)
}

private fun readPipesInfo(jsonResult: JsonObject): SessionInfo {
val langServiceReadPipeName = jsonResult.get("languageServiceReadPipeName")?.asString
val langServiceWritePipeName = jsonResult.get("languageServiceWritePipeName")?.asString
val debugServiceReadPipeName = jsonResult.get("debugServiceReadPipeName")?.asString
val debugServiceWritePipeName = jsonResult.get("debugServiceWritePipeName")?.asString
val powerShellVersion = jsonResult.get("powerShellVersion")?.asString
val status = jsonResult.get("status")?.asString
private fun readPipeConnectionInfo(prefix: String, jsonResult: JsonObject): ConnectionInfo.Pipes? {
val readPipeAttr = "${prefix}ReadPipeName"
val writePipeAttr = "${prefix}WritePipeName"

//todo make types nullable
return SessionInfo.Pipes(langServiceReadPipeName ?: "", langServiceWritePipeName ?: "", debugServiceReadPipeName ?: "", debugServiceWritePipeName ?: "", powerShellVersion, status)
val readPipeName = jsonResult.get(readPipeAttr)?.asString
val writePipeName = jsonResult.get(writePipeAttr)?.asString

if (readPipeName == null && writePipeName == null) {
return null
}

return ConnectionInfo.Pipes(
readPipeName ?: error("$readPipeAttr attribute is missing in a session file."),
writePipeName ?: error("$writePipeAttr attribute is missing in a session file.")
)
}

private fun readSessionFile(sessionFile: File): SessionInfo? {
return try {
val line = Files.asCharSource(sessionFile, Charset.forName("utf8")).readFirstLine()
val jsonResult = JsonParser.parseString(line).asJsonObject
readPipesInfo(jsonResult) ?: readTcpInfo(jsonResult)
val powerShellVersion = jsonResult.get("powerShellVersion")?.asString
val status = jsonResult.get("status")?.asString
val langConnection = readPipeConnectionInfo("languageService", jsonResult) ?: readTcpConnectionInfo("languageService", jsonResult)
val debugConnection = readPipeConnectionInfo("debugService", jsonResult) ?: readTcpConnectionInfo("debugService", jsonResult)
return SessionInfo(powerShellVersion, status, langConnection, debugConnection)
} catch (e: Exception) {
logger.warn("Error reading/parsing session details file $sessionFile: $e")
null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import java.io.InputStream
import java.io.OutputStream

interface LanguageHostConnectionManager {
suspend fun establishConnection(): Pair<InputStream?, OutputStream?>
suspend fun establishConnection(): Pair<InputStream, OutputStream>?
suspend fun establishDebuggerConnection(): Pair<InputStream, OutputStream>?
fun closeConnection()
fun isConnected(): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,7 @@ class LanguageServerEndpoint(

return coroutineScope.async job@{
try {
val (inStream, outStream) = languageHostConnectionManager.establishConnection()
if (inStream == null || outStream == null) {
val (inStream, outStream) = languageHostConnectionManager.establishConnection() ?: run {
logger.warn("Connection creation to PowerShell language host failed for $rootPath")
onStartFailure()
return@job null
Expand Down

0 comments on commit 7891c1f

Please sign in to comment.