IJPL-176 move module intellij.platform.ijent to community and make it available for WSL

As for now, IJent is available only when running PyCharm Pro from sources. Other targets are coming later.

Also, this commit seems to resolve IDEA-330810.


Merge-request: IJ-MR-113871
Merged-by: Vladimir Lagunov <vladimir.lagunov@jetbrains.com>

GitOrigin-RevId: ccf417002f0ec32c8f9cc9b3808e831593c166c8
This commit is contained in:
Vladimir Lagunov
2023-08-28 14:30:27 +00:00
committed by intellij-monorepo-bot
parent 5149c8e339
commit 249febac61
8 changed files with 327 additions and 0 deletions

1
.idea/modules.xml generated
View File

@@ -890,6 +890,7 @@
<module fileurl="file://$PROJECT_DIR$/platform/platform-util-io/intellij.platform.ide.util.io.iml" filepath="$PROJECT_DIR$/platform/platform-util-io/intellij.platform.ide.util.io.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/platform-util-io-impl/intellij.platform.ide.util.io.impl.iml" filepath="$PROJECT_DIR$/platform/platform-util-io-impl/intellij.platform.ide.util.io.impl.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/platform-util-netty/intellij.platform.ide.util.netty.iml" filepath="$PROJECT_DIR$/platform/platform-util-netty/intellij.platform.ide.util.netty.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/ijent/intellij.platform.ijent.iml" filepath="$PROJECT_DIR$/platform/ijent/intellij.platform.ijent.iml" />
<module fileurl="file://$PROJECT_DIR$/images/intellij.platform.images.iml" filepath="$PROJECT_DIR$/images/intellij.platform.images.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/build-scripts/icons/intellij.platform.images.build.iml" filepath="$PROJECT_DIR$/platform/build-scripts/icons/intellij.platform.images.build.iml" />
<module fileurl="file://$PROJECT_DIR$/images/intellij.platform.images.copyright/intellij.platform.images.copyright.iml" filepath="$PROJECT_DIR$/images/intellij.platform.images.copyright/intellij.platform.images.copyright.iml" />

View File

@@ -33,5 +33,6 @@
<orderEntry type="module" module-name="intellij.platform.workspace.jps" />
<orderEntry type="module" module-name="intellij.platform.backend.workspace" />
<orderEntry type="module" module-name="intellij.platform.diagnostic" />
<orderEntry type="module" module-name="intellij.platform.ijent" />
</component>
</module>

View File

@@ -0,0 +1,69 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
/**
* Later, this file will be moved close to `WSLDistribution`.
*/
@file:JvmName("IjentWslLauncher")
package com.intellij.execution.wsl.ijent
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.wsl.WSLCommandLineOptions
import com.intellij.execution.wsl.WSLDistribution
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.debug
import com.intellij.openapi.project.Project
import com.intellij.platform.ijent.IjentApi
import com.intellij.platform.ijent.IjentExecFileProvider
import com.intellij.platform.ijent.IjentSessionProvider
import kotlinx.coroutines.CoroutineScope
import kotlin.io.path.absolutePathString
suspend fun deployAndLaunchIjent(
ijentCoroutineScope: CoroutineScope,
project: Project?,
wslDistribution: WSLDistribution,
wslCommandLineOptions: WSLCommandLineOptions = WSLCommandLineOptions(),
): IjentApi {
val ijentBinary = IjentExecFileProvider.instance().getIjentBinary(IjentExecFileProvider.SupportedPlatform.X86_64__LINUX)
val wslIjentBinary = wslDistribution.getWslPath(ijentBinary.absolutePathString())
val (debuggingLogLevel, backtrace) = when {
LOG.isTraceEnabled -> "trace" to true
LOG.isDebugEnabled -> "debug" to true
else -> "info" to false
}
val commandLine = GeneralCommandLine(listOfNotNull(
// It's supposed that WslDistribution always converts commands into SHELL.
// There's no strict reason to call 'exec', just a tiny optimization.
"exec",
"/usr/bin/env",
"RUST_LOG=ijent=$debuggingLogLevel",
if (backtrace) "RUST_BACKTRACE=1" else null,
// "gdbserver", "0.0.0.0:12345", // https://sourceware.org/gdb/onlinedocs/gdb/Connecting.html
wslIjentBinary,
"grpc-stdio-server",
))
wslDistribution.patchCommandLine(commandLine, project, wslCommandLineOptions)
LOG.debug {
"Going to launch IJent: ${commandLine.commandLineString}"
}
val process = commandLine.createProcess()
try {
return IjentSessionProvider.connect(ijentCoroutineScope, process)
}
catch (err: Throwable) {
try {
process.destroy()
}
catch (err2: Throwable) {
err.addSuppressed(err)
}
throw err
}
}
private val LOG = Logger.getInstance("com.intellij.platform.ijent.IjentWslLauncher")

View File

@@ -0,0 +1,65 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.execution.wsl.ijent
import com.intellij.execution.wsl.WslDistributionManager
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.asContextElement
import com.intellij.openapi.diagnostic.runAndLogException
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.progress.ModalTaskOwner
import com.intellij.openapi.progress.TaskCancellation
import com.intellij.openapi.progress.withModalProgress
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.ui.Messages
import com.intellij.platform.ijent.IjentApi
import com.intellij.util.childScope
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.consumeEach
import java.io.ByteArrayOutputStream
@Suppress("DialogTitleCapitalization", "HardCodedStringLiteral")
class IjentWslVerificationAction : DumbAwareAction() {
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
override fun update(e: AnActionEvent) {
with(e.presentation) {
isEnabledAndVisible = ApplicationManager.getApplication().isInternal
isEnabled = isEnabled && e.project != null
}
}
@OptIn(DelicateCoroutinesApi::class) // Doesn't matter for a trivial test utility.
override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
val logger = thisLogger()
GlobalScope.launch {
logger.runAndLogException {
withModalProgress(ModalTaskOwner.project(project), e.presentation.text, TaskCancellation.cancellable()) {
val wslDistribution = WslDistributionManager.getInstance().installedDistributions.first()
coroutineScope {
val ijent = deployAndLaunchIjent(
ijentCoroutineScope = childScope(),
project = null,
wslDistribution = wslDistribution,
)
val process = when (val p = ijent.executeProcess("uname", "-a")) {
is IjentApi.ExecuteProcessResult.Failure -> error(p)
is IjentApi.ExecuteProcessResult.Success -> p.process
}
val stdout = ByteArrayOutputStream()
process.stdout.consumeEach(stdout::write)
withContext(Dispatchers.EDT + ModalityState.any().asContextElement()) {
Messages.showInfoMessage(stdout.toString(), "IJent on $wslDistribution: uname -a")
}
coroutineContext.cancelChildren()
}
}
}
}
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="library" name="kotlinx-coroutines-core" level="project" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.platform.extensions" />
</component>
</module>

View File

@@ -0,0 +1,15 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.ijent
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlin.coroutines.CoroutineContext
// TODO It is a copy-paste from Fleet, and it's better be generalized and put into some generic place.
fun CoroutineScope.coroutineNameAppended(name: String, separator: String = " > "): CoroutineContext =
coroutineContext.coroutineNameAppended(name, separator)
fun CoroutineContext.coroutineNameAppended(name: String, separator: String = " > "): CoroutineContext {
val parentName = this[CoroutineName]?.name
return CoroutineName(if (parentName == null) name else parentName + separator + name)
}

View File

@@ -0,0 +1,18 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.ijent
import com.intellij.openapi.components.serviceAsync
import java.nio.file.Path
interface IjentExecFileProvider {
companion object {
suspend fun instance(): IjentExecFileProvider = serviceAsync()
}
enum class SupportedPlatform {
X86_64__LINUX,
X86_64__WINDOWS,
}
suspend fun getIjentBinary(targetPlatform: SupportedPlatform): Path
}

View File

@@ -0,0 +1,143 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.ijent
import com.intellij.openapi.components.serviceAsync
import com.intellij.openapi.diagnostic.debug
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.diagnostic.trace
import com.intellij.util.attachAsChildTo
import com.intellij.util.namedChildScope
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel
import org.jetbrains.annotations.ApiStatus.OverrideOnly
import java.io.InputStream
import java.io.OutputStream
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicLong
/**
* Given that there is some IJent process launched, this extension gets handles to stdin+stdout of the process and returns
* an [IjentApi] instance for calling procedures on IJent side.
*/
interface IjentSessionProvider {
@get:OverrideOnly
val epCoroutineScope: CoroutineScope
/**
* When calling the method, there's no need to wire [communicationCoroutineScope] to [epCoroutineScope],
* since it is already performed by factory methods.
*/
@OverrideOnly
suspend fun connect(
id: Long,
communicationCoroutineScope: CoroutineScope,
inputStream: InputStream,
outputStream: OutputStream,
): IjentApi
companion object {
private val LOG = logger<IjentSessionProvider>()
private val counter = AtomicLong()
/**
* The session exits when one of the following happens:
* * The job corresponding to [communicationCoroutineScope] is finished.
* * [epCoroutineScope] is finished.
* * [inputStream] is closed.
*/
suspend fun connect(communicationCoroutineScope: CoroutineScope, process: Process): IjentApi {
val provider = serviceAsync<IjentSessionProvider>()
val id = counter.getAndIncrement()
val label = "IJent #$id"
val epCoroutineScope = provider.epCoroutineScope
val childScope = communicationCoroutineScope
.namedChildScope(label, supervisor = false)
.apply { attachAsChildTo(epCoroutineScope) }
childScope.launch(Dispatchers.IO + childScope.coroutineNameAppended("$label > watchdog")) {
while (true) {
if (process.waitFor(10, TimeUnit.MILLISECONDS)) {
val exitValue = process.exitValue()
LOG.debug { "$label exit code $exitValue" }
check(exitValue == 0) { "Process has exited with code $exitValue" }
cancel()
break
}
delay(100)
}
}
childScope.launch(Dispatchers.IO + childScope.coroutineNameAppended("$label > finalizer")) {
try {
awaitCancellation()
}
catch (err: Exception) {
LOG.debug(err) { "$label is going to be terminated due to receiving an error" }
throw err
}
finally {
if (process.isAlive) {
GlobalScope.launch(Dispatchers.IO + coroutineNameAppended("actual destruction")) {
try {
if (process.waitFor(5, TimeUnit.SECONDS)) {
LOG.debug { "$label exit code ${process.exitValue()}" }
}
}
finally {
if (process.isAlive) {
LOG.debug { "The process $label is still alive, it will be killed" }
process.destroy()
}
}
}
GlobalScope.launch(Dispatchers.IO) {
LOG.debug { "Closing stdin of $label" }
process.outputStream.close()
}
}
}
}
val processScopeNamePrefix = childScope.coroutineContext[CoroutineName]?.let { "$it >" } ?: ""
epCoroutineScope.launch(Dispatchers.IO) {
withContext(coroutineNameAppended("$processScopeNamePrefix $label > logger")) {
process.errorReader().use { errorReader ->
for (line in errorReader.lineSequence()) {
// TODO It works incorrectly with multiline log messages.
when (line.splitToSequence(' ').drop(1).take(1).firstOrNull()) {
"TRACE" -> LOG.trace { "$label log: $line" }
"DEBUG" -> LOG.debug { "$label log: $line" }
"INFO" -> LOG.info("$label log: $line")
"WARN" -> LOG.warn("$label log: $line")
"ERROR" -> LOG.error("$label log: $line")
else -> LOG.trace { "$label log: $line" }
}
yield()
}
}
}
}
return provider.connect(id, childScope, process.inputStream, process.outputStream)
}
}
}
interface IjentApi {
suspend fun executeProcess(exe: String, vararg args: String, env: Map<String, String> = emptyMap()): ExecuteProcessResult
sealed interface ExecuteProcessResult {
class Success(val process: IjentChildProcess) : ExecuteProcessResult
data class Failure(val errno: Int, val message: String) : ExecuteProcessResult
}
}
interface IjentChildProcess {
val pid: Int
val stdin: SendChannel<ByteArray>
val stdout: ReceiveChannel<ByteArray>
val stderr: ReceiveChannel<ByteArray>
val exitCode: Deferred<Int>
suspend fun sendSignal(signal: Int) // TODO Use a separate class for signals.
}