From cc2954797c1bfa08e8bf8b7fdafaca4bf25a9b40 Mon Sep 17 00:00:00 2001 From: Andrii Zinchenko Date: Mon, 21 Jul 2025 12:13:42 +0200 Subject: [PATCH] [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 --- .../platform/eel/provider/EelProvider.kt | 7 +- .../intellij/platform/eel/EelDescriptor.kt | 4 +- .../platform/eel/path/EelAbsolutePathTest.kt | 2 +- .../ijent/AbstractIjentVerificationAction.kt | 10 +- .../intellij/platform/ijent/IjentDeployer.kt | 57 ++---- .../platform/ijent/IjentSessionRegistry.kt | 10 +- .../platform/ijent/spi/DeployedIjent.kt | 20 -- .../ijent/spi/IjentDeployingStrategy.kt | 8 - .../ijent/spi/IjentSessionProvider.kt | 36 +--- .../ide/impl/wsl/ProductionWslIjentManager.kt | 16 +- .../platform/ide/impl/wsl/WslEelProvider.kt | 20 +- .../ide/impl/wsl/WslIjentDeployingStrategy.kt | 7 - .../platform/ide/impl/wsl/WslIjentUtil.kt | 178 +----------------- .../intellij/execution/wsl/WslIjentManager.kt | 24 ++- .../intellij/execution/wsl/WslIjentUtil.kt | 5 +- .../execution/wsl/WSLDistributionTest.kt | 5 +- .../junit5/eel/src/impl/EelTestDescriptor.kt | 2 +- .../systemPython/systemPythonServiceImpl.kt | 10 +- 18 files changed, 112 insertions(+), 309 deletions(-) delete mode 100644 platform/ijent/src/com/intellij/platform/ijent/spi/DeployedIjent.kt diff --git a/platform/eel-provider/src/com/intellij/platform/eel/provider/EelProvider.kt b/platform/eel-provider/src/com/intellij/platform/eel/provider/EelProvider.kt index 7de6a4cd1c90..001dbdd579a7 100644 --- a/platform/eel-provider/src/com/intellij/platform/eel/provider/EelProvider.kt +++ b/platform/eel-provider/src/com/intellij/platform/eel/provider/EelProvider.kt @@ -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 diff --git a/platform/eel/src/com/intellij/platform/eel/EelDescriptor.kt b/platform/eel/src/com/intellij/platform/eel/EelDescriptor.kt index b6a384eeb978..bc753c1e2f58 100644 --- a/platform/eel/src/com/intellij/platform/eel/EelDescriptor.kt +++ b/platform/eel/src/com/intellij/platform/eel/EelDescriptor.kt @@ -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) } /** diff --git a/platform/eel/tests/com/intellij/platform/eel/path/EelAbsolutePathTest.kt b/platform/eel/tests/com/intellij/platform/eel/path/EelAbsolutePathTest.kt index 70bdd1e8789f..082f25f1bafe 100644 --- a/platform/eel/tests/com/intellij/platform/eel/path/EelAbsolutePathTest.kt +++ b/platform/eel/tests/com/intellij/platform/eel/path/EelAbsolutePathTest.kt @@ -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() } } diff --git a/platform/execution-impl/src/com/intellij/execution/wsl/ijent/AbstractIjentVerificationAction.kt b/platform/execution-impl/src/com/intellij/execution/wsl/ijent/AbstractIjentVerificationAction.kt index f848acac3292..32433e8e7be8 100644 --- a/platform/execution-impl/src/com/intellij/execution/wsl/ijent/AbstractIjentVerificationAction.kt +++ b/platform/execution-impl/src/com/intellij/execution/wsl/ijent/AbstractIjentVerificationAction.kt @@ -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().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 + protected abstract suspend fun deployingStrategy(ijentProcessScope: CoroutineScope): Triple companion object { protected val LOG = logger() diff --git a/platform/ijent/src/com/intellij/platform/ijent/IjentDeployer.kt b/platform/ijent/src/com/intellij/platform/ijent/IjentDeployer.kt index 3b5c29c42f3e..04486e74d15c 100644 --- a/platform/ijent/src/com/intellij/platform/ijent/IjentDeployer.kt +++ b/platform/ijent/src/com/intellij/platform/ijent/IjentDeployer.kt @@ -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 { + 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 = +suspend fun IjentDeployingStrategy.createIjentSession(): IjentSession = 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(getConnectionStrategy(), remotePathToBinary, targetPlatform, mediator) } finally { close() diff --git a/platform/ijent/src/com/intellij/platform/ijent/IjentSessionRegistry.kt b/platform/ijent/src/com/intellij/platform/ijent/IjentSessionRegistry.kt index 3927d059dc6f..a30cf6d25a5d 100644 --- a/platform/ijent/src/com/intellij/platform/ijent/IjentSessionRegistry.kt +++ b/platform/ijent/src/com/intellij/platform/ijent/IjentSessionRegistry.kt @@ -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?, + val factory: suspend (ijentId: IjentId) -> IjentSession, + val deferred: Deferred>?, 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, ): 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 { 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? = when { + val reusedOldDeferred: Deferred>? = when { oldDeferred == null -> null oldBundle.oneOff -> oldDeferred !oldDeferred.isCompleted -> oldDeferred diff --git a/platform/ijent/src/com/intellij/platform/ijent/spi/DeployedIjent.kt b/platform/ijent/src/com/intellij/platform/ijent/spi/DeployedIjent.kt deleted file mode 100644 index 7808117db6a8..000000000000 --- a/platform/ijent/src/com/intellij/platform/ijent/spi/DeployedIjent.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/platform/ijent/src/com/intellij/platform/ijent/spi/IjentDeployingStrategy.kt b/platform/ijent/src/com/intellij/platform/ijent/spi/IjentDeployingStrategy.kt index ef1142f086ac..fc1377e1c3c5 100644 --- a/platform/ijent/src/com/intellij/platform/ijent/spi/IjentDeployingStrategy.kt +++ b/platform/ijent/src/com/intellij/platform/ijent/spi/IjentDeployingStrategy.kt @@ -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, diff --git a/platform/ijent/src/com/intellij/platform/ijent/spi/IjentSessionProvider.kt b/platform/ijent/src/com/intellij/platform/ijent/spi/IjentSessionProvider.kt index 9834c83f1077..3f93f4cd4af5 100644 --- a/platform/ijent/src/com/intellij/platform/ijent/spi/IjentSessionProvider.kt +++ b/platform/ijent/src/com/intellij/platform/ijent/spi/IjentSessionProvider.kt @@ -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 createIjentSession(strategy: IjentConnectionStrategy, binaryPath: String, platform: EelPlatform, mediator: IjentSessionMediator): IjentSession { 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 +} \ No newline at end of file diff --git a/platform/platform-impl/eel/src/com/intellij/platform/ide/impl/wsl/ProductionWslIjentManager.kt b/platform/platform-impl/eel/src/com/intellij/platform/ide/impl/wsl/ProductionWslIjentManager.kt index 99dfdff307dd..db4ca16a30a3 100644 --- a/platform/platform-impl/eel/src/com/intellij/platform/ide/impl/wsl/ProductionWslIjentManager.kt +++ b/platform/platform-impl/eel/src/com/intellij/platform/ide/impl/wsl/ProductionWslIjentManager.kt @@ -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 { diff --git a/platform/platform-impl/eel/src/com/intellij/platform/ide/impl/wsl/WslEelProvider.kt b/platform/platform-impl/eel/src/com/intellij/platform/ide/impl/wsl/WslEelProvider.kt index ab82f7942ad0..a57e3f009c47 100644 --- a/platform/platform-impl/eel/src/com/intellij/platform/ide/impl/wsl/WslEelProvider.kt +++ b/platform/platform-impl/eel/src/com/intellij/platform/ide/impl/wsl/WslEelProvider.kt @@ -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 get() { @@ -47,8 +48,8 @@ private val WSLDistribution.roots: Set 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) diff --git a/platform/platform-impl/eel/src/com/intellij/platform/ide/impl/wsl/WslIjentDeployingStrategy.kt b/platform/platform-impl/eel/src/com/intellij/platform/ide/impl/wsl/WslIjentDeployingStrategy.kt index 4ea00aa69cb8..28ea5330ac13 100644 --- a/platform/platform-impl/eel/src/com/intellij/platform/ide/impl/wsl/WslIjentDeployingStrategy.kt +++ b/platform/platform-impl/eel/src/com/intellij/platform/ide/impl/wsl/WslIjentDeployingStrategy.kt @@ -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 { diff --git a/platform/platform-impl/eel/src/com/intellij/platform/ide/impl/wsl/WslIjentUtil.kt b/platform/platform-impl/eel/src/com/intellij/platform/ide/impl/wsl/WslIjentUtil.kt index 0eb59bdf1d1a..8dc56503b30f 100644 --- a/platform/platform-impl/eel/src/com/intellij/platform/ide/impl/wsl/WslIjentUtil.kt +++ b/platform/platform-impl/eel/src/com/intellij/platform/ide/impl/wsl/WslIjentUtil.kt @@ -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 { 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 = - 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 - 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") } \ No newline at end of file + ).createIjentSession() +} \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/execution/wsl/WslIjentManager.kt b/platform/platform-impl/src/com/intellij/execution/wsl/WslIjentManager.kt index c7a929065c01..46db9a67e964 100644 --- a/platform/platform-impl/src/com/intellij/execution/wsl/WslIjentManager.kt +++ b/platform/platform-impl/src/com/intellij/execution/wsl/WslIjentManager.kt @@ -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", diff --git a/platform/platform-impl/src/com/intellij/execution/wsl/WslIjentUtil.kt b/platform/platform-impl/src/com/intellij/execution/wsl/WslIjentUtil.kt index 8de1120cfc57..1dcb1af424f2 100644 --- a/platform/platform-impl/src/com/intellij/execution/wsl/WslIjentUtil.kt +++ b/platform/platform-impl/src/com/intellij/execution/wsl/WslIjentUtil.kt @@ -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 = 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() diff --git a/platform/platform-tests/testSrc/com/intellij/execution/wsl/WSLDistributionTest.kt b/platform/platform-tests/testSrc/com/intellij/execution/wsl/WSLDistributionTest.kt index 2943df0463c0..6d871c060543 100644 --- a/platform/platform-tests/testSrc/com/intellij/execution/wsl/WSLDistributionTest.kt +++ b/platform/platform-tests/testSrc/com/intellij/execution/wsl/WSLDistributionTest.kt @@ -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() } } diff --git a/platform/testFramework/junit5/eel/src/impl/EelTestDescriptor.kt b/platform/testFramework/junit5/eel/src/impl/EelTestDescriptor.kt index ba10abe345de..d63d4f627b47 100644 --- a/platform/testFramework/junit5/eel/src/impl/EelTestDescriptor.kt +++ b/platform/testFramework/junit5/eel/src/impl/EelTestDescriptor.kt @@ -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 { diff --git a/python/services/system-python/src/com/intellij/python/community/services/systemPython/systemPythonServiceImpl.kt b/python/services/system-python/src/com/intellij/python/community/services/systemPython/systemPythonServiceImpl.kt index 114e16326526..58618146b787 100644 --- a/python/services/system-python/src/com/intellij/python/community/services/systemPython/systemPythonServiceImpl.kt +++ b/python/services/system-python/src/com/intellij/python/community/services/systemPython/systemPythonServiceImpl.kt @@ -50,13 +50,13 @@ internal suspend fun getCacheTimeout(): Duration? = @Internal internal class SystemPythonServiceImpl(scope: CoroutineScope) : SystemPythonService, SimplePersistentStateComponent(MyServiceState()) { private val findPythonsMutex = Mutex() - private val _cacheImpl: CompletableDeferred?> = CompletableDeferred() + private val _cacheImpl: CompletableDeferred?> = CompletableDeferred() private suspend fun cache() = _cacheImpl.await() init { scope.launch { _cacheImpl.complete(getCacheTimeout()?.let { interval -> - Cache(scope, interval) { eelDescriptor -> + Cache(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()