mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-04-19 21:11:28 +07:00
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:
committed by
intellij-monorepo-bot
parent
5149c8e339
commit
249febac61
1
.idea/modules.xml
generated
1
.idea/modules.xml
generated
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
@@ -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")
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
platform/ijent/intellij.platform.ijent.iml
Normal file
15
platform/ijent/intellij.platform.ijent.iml
Normal 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>
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
Reference in New Issue
Block a user