[eel][wsl] IJPL-196973 Compile error open target not source in wsl

- provide the possibility to get IjentApi instance by specific EelDescriptor

GitOrigin-RevId: 874542c0261cad5908da1fd10162f66800e97928
This commit is contained in:
Andrii Zinchenko
2025-07-23 15:04:29 +00:00
committed by intellij-monorepo-bot
parent 2fa1af099c
commit cc2954797c
18 changed files with 112 additions and 309 deletions
@@ -90,7 +90,7 @@ val localEel: LocalEelApi by lazy {
@ApiStatus.Internal
fun EelDescriptor.upgradeBlocking(): EelApi = toEelApiBlocking()
fun EelMachine.toEelApiBlocking(): EelApi = runBlockingMaybeCancellable { toEelApi() }
fun EelMachine.toEelApiBlocking(descriptor: EelDescriptor): EelApi = runBlockingMaybeCancellable { toEelApi(descriptor) }
@ApiStatus.Experimental
fun EelDescriptor.toEelApiBlocking(): EelApi {
@@ -113,7 +113,10 @@ data object LocalEelMachine : EelMachine {
}
}
override suspend fun toEelApi(): EelApi = localEel
override suspend fun toEelApi(descriptor: EelDescriptor): EelApi {
check(descriptor === LocalEelDescriptor) { "Wrong descriptor: $descriptor for machine: $this" }
return localEel
}
}
@ApiStatus.Experimental
@@ -52,7 +52,7 @@ interface EelMachine {
* Converts this machine into a [EelApi] — starts or reuses a running environment.
*/
@ApiStatus.Experimental
suspend fun toEelApi(): EelApi
suspend fun toEelApi(descriptor: EelDescriptor): EelApi
}
/**
@@ -136,7 +136,7 @@ interface EelDescriptor {
@ApiStatus.Experimental
suspend fun toEelApi(): EelApi {
return machine.toEelApi()
return machine.toEelApi(this)
}
/**
@@ -74,7 +74,7 @@ class EelAbsolutePathTest {
override val machine: EelMachine = object : EelMachine {
override val name: String = "mock"
override val osFamily: EelOsFamily = this@DummyEelDescriptor.osFamily
override suspend fun toEelApi(): EelApi {
override suspend fun toEelApi(descriptor: EelDescriptor): EelApi {
return Assertions.fail<Nothing>()
}
}
@@ -14,6 +14,7 @@ import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.diagnostic.runAndLogException
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.ui.Messages
import com.intellij.platform.eel.EelDescriptor
import com.intellij.platform.eel.spawnProcess
import com.intellij.platform.eel.provider.utils.copy
import com.intellij.platform.eel.provider.utils.asEelChannel
@@ -21,8 +22,9 @@ import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.platform.ide.progress.TaskCancellation
import com.intellij.platform.ide.progress.withModalProgress
import com.intellij.platform.ijent.IjentMissingBinary
import com.intellij.platform.ijent.IjentPosixApi
import com.intellij.platform.ijent.community.impl.nio.IjentNioFileSystemProvider
import com.intellij.platform.ijent.deploy
import com.intellij.platform.ijent.createIjentSession
import com.intellij.platform.ijent.spi.IjentDeployingStrategy
import kotlinx.coroutines.*
import org.jetbrains.annotations.ApiStatus.Internal
@@ -58,8 +60,8 @@ abstract class AbstractIjentVerificationAction : DumbAwareAction() {
try {
withModalProgress(modalTaskOwner, e.presentation.text, TaskCancellation.cancellable()) {
coroutineScope {
val (title, deployingStrategy) = deployingStrategy(this)
deployingStrategy.deploy().ijentApi.use { ijent ->
val (title, deployingStrategy, descriptor) = deployingStrategy(this)
deployingStrategy.createIjentSession<IjentPosixApi>().getIjentInstance(descriptor).use { ijent ->
coroutineScope {
launch {
val info = ijent.ijentProcessInfo
@@ -112,7 +114,7 @@ abstract class AbstractIjentVerificationAction : DumbAwareAction() {
}
}
protected abstract suspend fun deployingStrategy(ijentProcessScope: CoroutineScope): Pair<String, IjentDeployingStrategy>
protected abstract suspend fun deployingStrategy(ijentProcessScope: CoroutineScope): Triple<String, IjentDeployingStrategy, EelDescriptor>
companion object {
protected val LOG = logger<AbstractIjentVerificationAction>()
@@ -1,57 +1,34 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:JvmName("IjentDeployer")
package com.intellij.platform.ijent
import com.intellij.platform.ijent.spi.DeployedIjent
import com.intellij.platform.eel.EelDescriptor
import com.intellij.platform.ijent.spi.IjentDeployingStrategy
import com.intellij.platform.ijent.spi.connectToRunningIjent
import com.intellij.platform.ijent.spi.createIjentSession
/**
* Starts IJent on some machine, defined by [com.intellij.platform.ijent.spi.IjentDeployingStrategy].
*
* By default, the IJent executable exits when
* the coroutine scope of [IjentProcessMediator] from [IjentDeployingStrategy.createProcess] exits.
* [com.intellij.platform.ijent.IjentApi.close] may be used to terminate IJent earlier.
*
* TODO Either define thrown exceptions or return something like Result.
*/
suspend fun IjentDeployingStrategy.deploy(): DeployedIjent {
val (remotePathToBinary, ijentApi) = doDeploy()
return object : DeployedIjent {
override val ijentApi: IjentApi = ijentApi
override val remotePathToBinary: String = remotePathToBinary
interface IjentSession<T : IjentApi> {
val isRunning: Boolean
val remotePathToBinary: String // TODO Use IjentPath.Absolute.
suspend fun updateLogLevel()
fun close()
fun getIjentInstance(descriptor: EelDescriptor): T
enum class LogLevel {
INFO, DEBUG, TRACE, ALL
}
}
/** A specialized version of [com.intellij.platform.ijent.deploy] */
suspend fun IjentDeployingStrategy.Posix.deploy(): DeployedIjent.Posix {
val (remotePathToBinary, ijentApi) = doDeploy()
ijentApi as IjentPosixApi
return object : DeployedIjent.Posix {
override val ijentApi: IjentPosixApi = ijentApi
override val remotePathToBinary: String = remotePathToBinary
}
}
/** A specialized version of [com.intellij.platform.ijent.deploy] */
suspend fun IjentDeployingStrategy.Windows.deploy(): DeployedIjent.Windows {
val (remotePathToBinary, ijentApi) = doDeploy()
ijentApi as IjentWindowsApi
return object : DeployedIjent.Windows {
override val ijentApi: IjentWindowsApi = ijentApi
override val remotePathToBinary: String = remotePathToBinary
}
}
private suspend fun IjentDeployingStrategy.doDeploy(): Pair<String, IjentApi> =
suspend fun <T : IjentApi> IjentDeployingStrategy.createIjentSession(): IjentSession<T> =
try {
val targetPlatform = getTargetPlatform()
val targetDescriptor = getTargetDescriptor()
val remotePathToBinary = copyFile(IjentExecFileProvider.getInstance().getIjentBinary(targetPlatform))
val mediator = createProcess(remotePathToBinary)
val ijentApi = connectToRunningIjent(getConnectionStrategy(), targetPlatform, targetDescriptor, mediator)
remotePathToBinary to ijentApi
createIjentSession<T>(getConnectionStrategy(), remotePathToBinary, targetPlatform, mediator)
}
finally {
close()
@@ -18,8 +18,8 @@ class IjentSessionRegistry(private val coroutineScope: CoroutineScope) {
private val counter = AtomicLong()
private class IjentBundle(
val factory: suspend (ijentId: IjentId) -> IjentApi,
val deferred: Deferred<IjentApi>?,
val factory: suspend (ijentId: IjentId) -> IjentSession<IjentPosixApi>,
val deferred: Deferred<IjentSession<IjentPosixApi>>?,
val oneOff: Boolean,
)
@@ -33,7 +33,7 @@ class IjentSessionRegistry(private val coroutineScope: CoroutineScope) {
*/
fun register(
ijentName: String,
launcher: suspend (ijentId: IjentId) -> IjentApi,
launcher: suspend (ijentId: IjentId) -> IjentSession<IjentPosixApi>,
): IjentId {
val ijentId = IjentId("ijent-${counter.getAndIncrement()}-${ijentName.replace(Regex("[^A-Za-z0-9-]"), "-")}")
ijents[ijentId] = IjentBundle(launcher, null, oneOff = false)
@@ -65,7 +65,7 @@ class IjentSessionRegistry(private val coroutineScope: CoroutineScope) {
* An instance of [IjentApi] that has ever thrown [IjentUnavailableException] will never be returned by this function again.
*/
@OptIn(ExperimentalCoroutinesApi::class)
suspend fun get(ijentId: IjentId): IjentApi {
suspend fun get(ijentId: IjentId): IjentSession<IjentPosixApi> {
val bundle = ijents.compute(ijentId, @Suppress("NAME_SHADOWING") { ijentId, oldBundle ->
require(oldBundle != null) {
"Not registered: $ijentId"
@@ -73,7 +73,7 @@ class IjentSessionRegistry(private val coroutineScope: CoroutineScope) {
val oldDeferred = oldBundle.deferred
val reusedOldDeferred: Deferred<IjentApi>? = when {
val reusedOldDeferred: Deferred<IjentSession<IjentPosixApi>>? = when {
oldDeferred == null -> null
oldBundle.oneOff -> oldDeferred
!oldDeferred.isCompleted -> oldDeferred
@@ -1,20 +0,0 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.ijent.spi
import com.intellij.platform.ijent.IjentApi
import com.intellij.platform.ijent.IjentPosixApi
import com.intellij.platform.ijent.IjentWindowsApi
interface DeployedIjent {
val ijentApi: IjentApi
val remotePathToBinary: String // TODO Use IjentPath.Absolute.
interface Posix : DeployedIjent {
override val ijentApi: IjentPosixApi
}
interface Windows : DeployedIjent {
override val ijentApi: IjentWindowsApi
}
}
@@ -1,9 +1,7 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.ijent.spi
import com.intellij.platform.eel.EelDescriptor
import com.intellij.platform.eel.EelPlatform
import com.intellij.platform.ijent.deploy
import org.jetbrains.annotations.ApiStatus
import java.nio.file.Path
@@ -28,12 +26,6 @@ interface IjentDeployingStrategy {
*/
suspend fun getTargetPlatform(): EelPlatform
/**
* Description of the target environment. This descriptor is accepted by IntelliJ Platform API that works with Eel.
* todo: merge it with [getTargetPlatform]
*/
suspend fun getTargetDescriptor(): EelDescriptor
/**
* Defines a set of options for connecting to a running IJent
* This step is logically different from deployment,
@@ -2,12 +2,11 @@
package com.intellij.platform.ijent.spi
import com.intellij.openapi.components.serviceAsync
import com.intellij.platform.eel.EelDescriptor
import com.intellij.platform.eel.EelPlatform
import com.intellij.platform.ijent.IjentApi
import com.intellij.platform.ijent.IjentPosixApi
import com.intellij.platform.ijent.IjentSession
import com.intellij.platform.ijent.IjentSessionRegistry
import com.intellij.platform.ijent.IjentWindowsApi
import java.nio.file.Path
/**
* Given that there is some IJent process launched, this extension gets handles to stdin+stdout of the process and returns
@@ -20,9 +19,9 @@ interface IjentSessionProvider {
suspend fun connect(
strategy: IjentConnectionStrategy,
platform: EelPlatform,
descriptor: EelDescriptor,
binaryPath: String,
mediator: IjentSessionMediator,
): IjentApi
): IjentSession<*>
companion object {
suspend fun instanceAsync(): IjentSessionProvider = serviceAsync()
@@ -45,7 +44,7 @@ sealed class IjentStartupError : RuntimeException {
}
internal class DefaultIjentSessionProvider : IjentSessionProvider {
override suspend fun connect(strategy: IjentConnectionStrategy, platform: EelPlatform, descriptor: EelDescriptor, mediator: IjentSessionMediator): IjentApi {
override suspend fun connect(strategy: IjentConnectionStrategy, platform: EelPlatform, binaryPath: String, mediator: IjentSessionMediator): IjentSession<*> {
throw IjentStartupError.MissingImplPlugin()
}
}
@@ -56,25 +55,8 @@ internal class DefaultIjentSessionProvider : IjentSessionProvider {
*
* The process terminates automatically only when the IDE exits, or if [IjentApi.close] is called explicitly.
*/
suspend fun connectToRunningIjent(strategy: IjentConnectionStrategy, platform: EelPlatform, descriptor: EelDescriptor, mediator: IjentSessionMediator): IjentApi {
suspend fun <T : IjentApi> createIjentSession(strategy: IjentConnectionStrategy, binaryPath: String, platform: EelPlatform, mediator: IjentSessionMediator): IjentSession<T> {
mediator.myExitPolicy = IjentSessionMediator.ProcessExitPolicy.CHECK_CODE
return IjentSessionProvider.instanceAsync().connect(strategy, platform, descriptor, mediator)
}
/** A specialized overload of [connectToRunningIjent] */
suspend fun connectToRunningIjent(
strategy: IjentConnectionStrategy,
platform: EelPlatform.Posix,
descriptor: EelDescriptor,
mediator: IjentSessionMediator,
): IjentPosixApi =
connectToRunningIjent(strategy, platform as EelPlatform, descriptor, mediator) as IjentPosixApi
/** A specialized overload of [connectToRunningIjent] */
suspend fun connectToRunningIjent(
strategy: IjentConnectionStrategy,
platform: EelPlatform.Windows,
descriptor: EelDescriptor,
mediator: IjentSessionMediator,
): IjentWindowsApi =
connectToRunningIjent(strategy, platform as EelPlatform, descriptor, mediator) as IjentWindowsApi
@Suppress("UNCHECKED_CAST")
return IjentSessionProvider.instanceAsync().connect(strategy, platform, binaryPath, mediator) as IjentSession<T>
}
@@ -7,8 +7,11 @@ import com.intellij.execution.wsl.WslIjentManager
import com.intellij.openapi.components.serviceIfCreated
import com.intellij.openapi.progress.Cancellation
import com.intellij.openapi.project.Project
import com.intellij.platform.eel.EelDescriptor
import com.intellij.platform.eel.provider.getEelDescriptor
import com.intellij.platform.ijent.IjentId
import com.intellij.platform.ijent.IjentPosixApi
import com.intellij.platform.ijent.IjentSession
import com.intellij.platform.ijent.IjentSessionRegistry
import com.intellij.platform.ijent.spi.IjentThreadPool
import com.intellij.platform.util.coroutines.childScope
@@ -37,21 +40,22 @@ class ProductionWslIjentManager(private val scope: CoroutineScope) : WslIjentMan
)
}
override suspend fun getIjentApi(wslDistribution: WSLDistribution, project: Project?, rootUser: Boolean): IjentPosixApi {
override suspend fun getIjentApi(descriptor: EelDescriptor?, wslDistribution: WSLDistribution, project: Project?, rootUser: Boolean): IjentPosixApi {
val descriptor = (descriptor ?: project?.getEelDescriptor() ?: WslEelDescriptor(wslDistribution)) as WslEelDescriptor
val ijentSessionRegistry = IjentSessionRegistry.instanceAsync()
val ijentId = myCache.computeIfAbsent("""wsl:${wslDistribution.id}${if (rootUser) ":root" else ""}""") { ijentName ->
val ijentId = ijentSessionRegistry.register(ijentName) { ijentId ->
val ijent = deployAndLaunchIjent(
val ijentSession = wslDistribution.createIjentSession(
scope,
project,
ijentId.toString(),
wslDistribution,
wslCommandLineOptionsModifier = { it.setSudo(rootUser) },
)
scope.coroutineContext.job.invokeOnCompletion {
ijent.close()
ijentSession.close()
}
ijent
ijentSession
}
scope.coroutineContext.job.invokeOnCompletion {
ijentSessionRegistry.unregister(ijentId)
@@ -59,7 +63,7 @@ class ProductionWslIjentManager(private val scope: CoroutineScope) : WslIjentMan
}
ijentId
}
return ijentSessionRegistry.get(ijentId) as IjentPosixApi
return ijentSessionRegistry.get(ijentId).getIjentInstance(descriptor)
}
init {
@@ -36,6 +36,7 @@ import java.nio.file.*
import java.nio.file.FileSystems.getDefault
import java.util.concurrent.atomic.AtomicReference
import kotlin.io.path.Path
import kotlin.io.path.pathString
private val WSLDistribution.roots: Set<String>
get() {
@@ -47,8 +48,8 @@ private val WSLDistribution.roots: Set<String>
return localRoots
}
private suspend fun WSLDistribution.getIjent(): IjentPosixApi {
return WslIjentManager.instanceAsync().getIjentApi(this, null, false)
private suspend fun WSLDistribution.getIjent(descriptor: EelDescriptor): IjentPosixApi {
return WslIjentManager.instanceAsync().getIjentApi(descriptor, this, null, false)
}
@ApiStatus.Internal
@@ -230,7 +231,10 @@ class WslEelProvider : EelProvider {
false
}
}
WslIjentManager.instanceAsync().getIjentApi(wslPath.distribution, project, false)
val descriptor = WslEelDescriptor(wslPath.distribution, wslPath.wslRoot)
WslIjentManager.instanceAsync().getIjentApi(descriptor, wslPath.distribution, project, false)
(getDefault().provider() as MultiRoutingFileSystemProvider).theOnlyFileSystem.getBackend(wslPath.wslRoot + "\\")
}
@@ -268,8 +272,12 @@ class WslEelMachine(val distribution: WSLDistribution) : EelMachine {
override val osFamily: EelOsFamily = EelOsFamily.Posix
override val name: @NonNls String = "WSL: ${distribution.presentableName}"
override suspend fun toEelApi(): EelApi {
return distribution.getIjent()
override suspend fun toEelApi(descriptor: EelDescriptor): EelApi {
check(descriptor is WslEelDescriptor && descriptor.machine == this) {
"Wrong descriptor: $descriptor for machine: $this"
}
return distribution.getIjent(descriptor)
}
override fun equals(other: Any?): Boolean {
@@ -292,6 +300,8 @@ class WslEelMachine(val distribution: WSLDistribution) : EelMachine {
}
class WslEelDescriptor(val distribution: WSLDistribution, internal val fsRoot: String) : EelPathBoundDescriptor {
constructor(distribution: WSLDistribution): this(distribution, distribution.getUNCRootPath().pathString)
override val rootPath: Path get() = fsRoot.let(::Path)
override val machine: EelMachine = WslEelMachine(distribution)
@@ -46,13 +46,6 @@ class WslIjentDeployingStrategy(
return computeDetached { commandLine.createProcess() }
}
override suspend fun getTargetDescriptor(): EelDescriptor {
val root = project?.basePath?.let {
MultiRoutingFileSystem.sanitizeRoot(it)
}?.let(WslEelProvider::parsePath)?.first ?: distribution.getUNCRootPath().toString()
return WslEelDescriptor(distribution, root)
}
override suspend fun getConnectionStrategy(): IjentConnectionStrategy {
return object : IjentConnectionStrategy {
override suspend fun canUseVirtualSockets(): Boolean {
@@ -5,189 +5,27 @@
package com.intellij.platform.ide.impl.wsl
import com.intellij.execution.CommandLineUtil.posixQuote
import com.intellij.execution.ijent.IjentChildProcessAdapter
import com.intellij.execution.ijent.IjentChildPtyProcessAdapter
import com.intellij.execution.process.LocalPtyOptions
import com.intellij.execution.wsl.WSLCommandLineOptions
import com.intellij.execution.wsl.WSLDistribution
import com.intellij.execution.wsl.WslIjentManager
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.debug
import com.intellij.openapi.progress.runBlockingCancellable
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.io.FileUtil
import com.intellij.platform.eel.EelExecApi
import com.intellij.platform.eel.EelProcess
import com.intellij.platform.eel.path.EelPath
import com.intellij.platform.eel.spawnProcess
import com.intellij.platform.ijent.IjentPosixApi
import com.intellij.platform.ijent.deploy
import com.intellij.platform.ijent.spi.DeployedIjent
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.util.concurrency.annotations.RequiresBlockingContext
import com.intellij.util.suspendingLazy
import com.intellij.platform.ijent.IjentSession
import com.intellij.platform.ijent.createIjentSession
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.VisibleForTesting
internal suspend fun deployAndLaunchIjent(
@ApiStatus.Internal
suspend fun WSLDistribution.createIjentSession(
parentScope: CoroutineScope,
project: Project?,
ijentLabel: String,
wslDistribution: WSLDistribution,
wslCommandLineOptionsModifier: (WSLCommandLineOptions) -> Unit = {},
): IjentPosixApi =
deployAndLaunchIjentGettingPath(parentScope, project, ijentLabel, wslDistribution, wslCommandLineOptionsModifier).ijentApi
@VisibleForTesting
suspend fun deployAndLaunchIjentGettingPath(
parentScope: CoroutineScope,
project: Project?,
ijentLabel: String,
wslDistribution: WSLDistribution,
wslCommandLineOptionsModifier: (WSLCommandLineOptions) -> Unit = {},
): DeployedIjent.Posix {
): IjentSession<IjentPosixApi> {
return WslIjentDeployingStrategy(
scope = parentScope,
ijentLabel = ijentLabel,
distribution = wslDistribution,
distribution = this,
project = project,
wslCommandLineOptionsModifier = wslCommandLineOptionsModifier
).deploy()
}
/**
* An adapter for [com.intellij.platform.ijent.IjentExecApi.fetchLoginShellEnvVariables] for Java.
*/
@RequiresBackgroundThread
@RequiresBlockingContext
fun fetchLoginShellEnv(
wslIjentManager: WslIjentManager,
wslDistribution: WSLDistribution,
project: Project?,
rootUser: Boolean,
): Map<String, String> =
runBlockingCancellable {
wslIjentManager.getIjentApi(wslDistribution, project, rootUser).exec.fetchLoginShellEnvVariables()
}
/**
* Runs a process inside a WSL container defined by [wslDistribution] using IJent.
*
* [project] is supposed to be used only for showing notifications with appropriate modality. Therefore, it may be almost safely omitted.
*
* [processBuilder] chosen as a convenient adapter for already written code. The functions uses command line arguments, environment
* variables and the working directory defined by [processBuilder].
*
* The function ignores [ProcessBuilder.redirectInput], [ProcessBuilder.redirectOutput], [ProcessBuilder.redirectError] and similar
* methods. Stdin, stdout, and stderr are always piped. The caller MUST drain both [Process.getInputStream] and [Process.getErrorStream].
* Otherwise, the remote operating system may suspend the remote process due to buffer overflow.
*
* [ProcessBuilder.directory] is a Windows path, and the constructor of [java.io.File] can corrupt the path. Therefore,
* [WSLCommandLineOptions.getRemoteWorkingDirectory] is preferred over [ProcessBuilder.directory].
*/
@RequiresBackgroundThread
@RequiresBlockingContext
fun runProcessBlocking(
wslIjentManager: WslIjentManager,
project: Project?,
wslDistribution: WSLDistribution,
processBuilder: ProcessBuilder,
options: WSLCommandLineOptions,
ptyOptions: LocalPtyOptions?,
): Process = runBlockingCancellable {
val ijentApi = wslIjentManager.getIjentApi(wslDistribution, project, options.isSudo)
val args = processBuilder.command().toMutableList()
require(!options.isExecuteCommandInDefaultShell) {
"This API is not supposed to handle WSLCommandLineOptions.isExecuteCommandInDefaultShell"
}
val shell = suspendingLazy {
ijentApi.exec.fetchLoginShellEnvVariables()["SHELL"]
?: WSLDistribution.DEFAULT_SHELL
}
val shellInitCommands = options.initShellCommands.asReversed().toMutableList()
val explicitEnvironmentVariables: Map<String, String>
if (options.isExecuteCommandInShell && !options.isPassEnvVarsUsingInterop) {
explicitEnvironmentVariables = mapOf()
for ((name, value) in processBuilder.environment().entries.sortedBy { (key, _) -> key }) {
if (WSLDistribution.ENV_VARIABLE_NAME_PATTERN.matcher(name).matches()) {
shellInitCommands += "export ${posixQuote(name)}=${posixQuote(value)}"
}
else {
LOG.debug { "Can not pass environment variable (bad name): '$name'" }
}
}
}
else {
explicitEnvironmentVariables = processBuilder.environment()
}
options.remoteWorkingDirectory?.takeIf(String::isNotEmpty)?.let { remoteWorkingDirectory ->
// Although there's another and more straightforward way to specify the working directory, this code repeats the logic from
// `WSLDistribution.doPatchCommandLine`, just to not break someone's workflow.
shellInitCommands += "cd ${posixQuote(remoteWorkingDirectory)}"
}
if (options.isExecuteCommandInShell || shellInitCommands.isNotEmpty()) {
// The sequence of the argument should correspond the sequence from `com.intellij.execution.wsl.WSLDistribution#doPatchCommandLine`
// Although some argument may be mixed up de-facto, the function is covered by unit tests that check arguments using strict comparison.
val shellArgument = (shellInitCommands + args.joinToString(" ", transform = ::posixQuote)).joinToString(" && ")
args.clear()
args += shell.getValue()
if (options.isExecuteCommandInInteractiveShell) {
args += "-i"
}
if (options.isExecuteCommandInLoginShell) {
args += "-l"
}
args += "-c"
args += shellArgument
}
val exePath = FileUtil.toSystemIndependentName(args.removeFirst())
val interactionOptions = when {
ptyOptions != null -> with(ptyOptions) { EelExecApi.Pty(initialColumns, initialRows, !consoleMode) }
processBuilder.redirectErrorStream() -> EelExecApi.RedirectStdErr(to = EelExecApi.RedirectTo.STDOUT)
else -> null
}
val workingDirectory = processBuilder.directory()?.toPath()?.let { windowsWorkingDirectory ->
wslDistribution.getWslPath(windowsWorkingDirectory)
?: run {
LOG.warn("Working directory $windowsWorkingDirectory can't be mapped to WSL distribution ${wslDistribution.id}", Throwable())
null
}
}
val scope = @OptIn(DelicateCoroutinesApi::class) (wslIjentManager.processAdapterScope)
ijentApi.exec.spawnProcess(exePath)
.args(args)
.env(explicitEnvironmentVariables)
.interactionOptions(interactionOptions)
.workingDirectory(workingDirectory?.let { EelPath.parse(it, ijentApi.descriptor) })
.eelIt()
.toProcess(
coroutineScope = scope,
isPty = interactionOptions != null,
)
}
private fun EelProcess.toProcess(
coroutineScope: CoroutineScope,
isPty: Boolean,
): Process =
if (isPty)
IjentChildPtyProcessAdapter(coroutineScope, this)
else
IjentChildProcessAdapter(coroutineScope, this)
private val LOG by lazy { Logger.getInstance("com.intellij.execution.wsl.WslIjentUtil") }
).createIjentSession()
}
@@ -4,7 +4,9 @@ package com.intellij.execution.wsl
import com.intellij.openapi.components.service
import com.intellij.openapi.components.serviceAsync
import com.intellij.openapi.project.Project
import com.intellij.platform.eel.EelDescriptor
import com.intellij.platform.ijent.IjentPosixApi
import com.intellij.platform.ijent.IjentSession
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import org.jetbrains.annotations.ApiStatus
@@ -22,9 +24,27 @@ interface WslIjentManager {
val processAdapterScope: CoroutineScope
/**
* The returned instance is not supposed to be closed by the caller. [WslIjentManager] closes [IjentApi] by itself during shutdown.
* Acquires an [IjentPosixApi] for the given WSL distribution.
*
* ## Descriptor Resolution
* If [descriptor] is `null`, a fallback resolution is performed:
* - If [project] is provided, [getEelDescriptor] is called on the project to derive the [EelDescriptor].
* - If [project] is also `null`, a default descriptor is created from [wslDistribution] using [WslEelDescriptor].
*
* This ensures that an [EelDescriptor] is always passed to [IjentSession.getIjentInstance], which may influence
* environment configuration or file path resolution.
*
* ## Lifecycle
* - The returned [IjentPosixApi] is managed internally and **must not** be closed by the caller.
* - The underlying session is closed automatically when the enclosing coroutine scope completes.
*
* @param descriptor An optional descriptor pointing to a specific environment path.
* @param wslDistribution The target WSL distribution (e.g. Ubuntu, Debian).
* @param project An optional project, used as a fallback source for descriptor resolution.
* @param rootUser Whether the Ijent should run with root privileges (`sudo`).
* @return An initialized [IjentPosixApi] ready for interaction with the WSL environment.
*/
suspend fun getIjentApi(wslDistribution: WSLDistribution, project: Project?, rootUser: Boolean): IjentPosixApi
suspend fun getIjentApi(descriptor: EelDescriptor?, wslDistribution: WSLDistribution, project: Project?, rootUser: Boolean): IjentPosixApi
@Deprecated(
"Use WslIjentAvailabilityService.runWslCommandsViaIjent",
@@ -18,6 +18,7 @@ import com.intellij.platform.eel.EelExecApi
import com.intellij.platform.eel.EelProcess
import com.intellij.platform.eel.path.EelPath
import com.intellij.platform.eel.spawnProcess
import com.intellij.platform.ijent.IjentPosixApi
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.util.concurrency.annotations.RequiresBlockingContext
import com.intellij.util.suspendingLazy
@@ -37,7 +38,7 @@ fun fetchLoginShellEnv(
rootUser: Boolean,
): Map<String, String> =
runBlockingCancellable {
wslIjentManager.getIjentApi(wslDistribution, project, rootUser).exec.fetchLoginShellEnvVariables()
wslIjentManager.getIjentApi(null, wslDistribution, project, rootUser).exec.fetchLoginShellEnvVariables()
}
/**
@@ -65,7 +66,7 @@ fun runProcessBlocking(
options: WSLCommandLineOptions,
ptyOptions: LocalPtyOptions?,
): Process = runBlockingCancellable {
val ijentApi = wslIjentManager.getIjentApi(wslDistribution, project, options.isSudo)
val ijentApi: IjentPosixApi = wslIjentManager.getIjentApi(null, wslDistribution, project, options.isSudo)
val args = processBuilder.command().toMutableList()
@@ -19,6 +19,7 @@ import com.intellij.platform.eel.EelExecApi.ExternalCliEntrypoint
import com.intellij.platform.eel.path.EelPath
import com.intellij.platform.ijent.IjentPosixApi
import com.intellij.platform.ijent.IjentProcessInfo
import com.intellij.platform.ijent.IjentSession
import com.intellij.platform.ijent.IjentTunnelsPosixApi
import com.intellij.platform.ijent.fs.IjentFileSystemPosixApi
import com.intellij.testFramework.junit5.TestApplication
@@ -485,7 +486,7 @@ class WSLDistributionTest {
@DelicateCoroutinesApi
override val processAdapterScope: CoroutineScope = scope
override suspend fun getIjentApi(wslDistribution: WSLDistribution, project: Project?, rootUser: Boolean): IjentPosixApi {
override suspend fun getIjentApi(descriptor: EelDescriptor?, wslDistribution: WSLDistribution, project: Project?, rootUser: Boolean): IjentPosixApi {
require(wslDistribution == mockWslDistribution) { "$wslDistribution != $mockWslDistribution" }
return MockIjentApi(adapter, rootUser)
}
@@ -526,7 +527,7 @@ private class MockIjentApi(private val adapter: GeneralCommandLine, val rootUser
override val name: @NonNls String = "mock"
override val osFamily: EelOsFamily = this@MockIjentApi.platform.osFamily
override suspend fun toEelApi(): EelApi {
override suspend fun toEelApi(descriptor: EelDescriptor): EelApi {
throw UnsupportedOperationException()
}
}
@@ -13,7 +13,7 @@ internal class EelTestDescriptor(override val rootPath: Path, val id: String, ov
override val machine: EelMachine = object : EelMachine {
override val name: @NonNls String = "mock $id"
override val osFamily: EelOsFamily get() = this@EelTestDescriptor.osFamily
override suspend fun toEelApi(): EelApi = apiProvider()
override suspend fun toEelApi(descriptor: EelDescriptor): EelApi = apiProvider()
}
override suspend fun toEelApi(): EelApi {
@@ -50,13 +50,13 @@ internal suspend fun getCacheTimeout(): Duration? =
@Internal
internal class SystemPythonServiceImpl(scope: CoroutineScope) : SystemPythonService, SimplePersistentStateComponent<MyServiceState>(MyServiceState()) {
private val findPythonsMutex = Mutex()
private val _cacheImpl: CompletableDeferred<Cache<EelMachine, SystemPython>?> = CompletableDeferred()
private val _cacheImpl: CompletableDeferred<Cache<EelDescriptor, SystemPython>?> = CompletableDeferred()
private suspend fun cache() = _cacheImpl.await()
init {
scope.launch {
_cacheImpl.complete(getCacheTimeout()?.let { interval ->
Cache<EelMachine, SystemPython>(scope, interval) { eelDescriptor ->
Cache<EelDescriptor, SystemPython>(scope, interval) { eelDescriptor ->
searchPythonsPhysicallyNoCache(eelDescriptor.toEelApi())
}
})
@@ -68,7 +68,7 @@ internal class SystemPythonServiceImpl(scope: CoroutineScope) : SystemPythonServ
.getOr(PySystemPythonBundle.message("py.system.python.service.python.is.broken", pythonPath)) { return it }
val systemPython = SystemPython(pythonWithLangLevel, null)
state.userProvidedPythons.add(pythonPath.pathString)
cache()?.get(pythonPath.getEelDescriptor().machine)?.add(systemPython)
cache()?.get(pythonPath.getEelDescriptor())?.add(systemPython)
return Result.success(systemPython)
}
@@ -81,10 +81,10 @@ internal class SystemPythonServiceImpl(scope: CoroutineScope) : SystemPythonServ
cache.startUpdate()
if (forceRefresh) {
logger.info("pythons refresh requested")
cache.updateCache(eelApi.descriptor.machine) // Update cache and suspend till update finished
cache.updateCache(eelApi.descriptor) // Update cache and suspend till update finished
}
else {
cache.get(eelApi.descriptor.machine)
cache.get(eelApi.descriptor)
}.sorted()
} ?: searchPythonsPhysicallyNoCache(eelApi).sorted()