Eel (and partially PY-82160): Introduce API to provide a scope for process.

No process lives on an island. Process might be bound to a certain scope. It must not outlive this scope.

GitOrigin-RevId: 6ad86e67890c0684bdee54d0d314bf2b6760774d
This commit is contained in:
Ilya.Kazakevich
2025-07-05 03:02:06 +00:00
committed by intellij-monorepo-bot
parent 2f61fa2a7b
commit 9cf4ff7e6e
8 changed files with 121 additions and 2 deletions
@@ -13,6 +13,7 @@ import com.intellij.openapi.util.SystemInfo
import com.intellij.platform.eel.*
import com.intellij.platform.eel.path.EelPath
import com.intellij.platform.eel.provider.LocalEelDescriptor
import com.intellij.platform.eel.provider.utils.bindToScope
import com.intellij.util.EnvironmentUtil
import com.pty4j.PtyProcess
import kotlinx.coroutines.Dispatchers
@@ -30,10 +31,12 @@ class EelLocalExecPosixApi : EelExecPosixApi {
generatedBuilder: EelExecApi.ExecuteProcessOptions,
): EelPosixProcess {
val process = executeImpl(generatedBuilder)
return if (process is PtyProcess)
val r = if (process is PtyProcess)
LocalEelPosixProcess.create(process, process::setWinSize)
else
LocalEelPosixProcess.create(process, null)
generatedBuilder.scope?.let { r.bindToScope(it) }
return r
}
override val descriptor: EelDescriptor = LocalEelDescriptor
@@ -59,10 +62,12 @@ class EelLocalExecWindowsApi : EelExecWindowsApi {
): EelWindowsProcess {
val process = executeImpl(generatedBuilder)
val commandLineForDebug = (listOf(generatedBuilder.exe) + generatedBuilder.args).joinToString(" ")
return if (process is PtyProcess)
val r = if (process is PtyProcess)
LocalEelWindowsProcess.create(process, process::setWinSize, commandLineForDebug)
else
LocalEelWindowsProcess.create(process, null, commandLineForDebug)
generatedBuilder.scope?.let { r.bindToScope(it) }
return r
}
override val descriptor: EelDescriptor = LocalEelDescriptor
@@ -0,0 +1,48 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.eel.provider.utils
import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.platform.eel.EelProcess
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
private val logger = fileLogger()
@ApiStatus.Internal
fun EelProcess.bindToScope(scope: CoroutineScope) {
val context = CoroutineName("Waiting for process $this") + Dispatchers.IO
if (!scope.isActive) {
logger.warn("Scope $scope is dead, killing process $this")
scope.launch(context, start = CoroutineStart.UNDISPATCHED) {
killAndJoin()
}
}
scope.launch(context, start = CoroutineStart.UNDISPATCHED) {
try {
exitCode.await()
}
catch (e: CancellationException) {
killAndJoin()
throw e
}
}
}
private suspend fun EelProcess.killAndJoin() {
withContext(NonCancellable) {
logger.warn("Sending kill to $this")
kill()
logger.warn("Waiting $this to exit")
exitCode.await()
logger.warn("Process $this died")
}
}
+5
View File
@@ -53,6 +53,7 @@
- *:getEnv():java.util.Map
- *a:getExe():java.lang.String
- *:getInteractionOptions():com.intellij.platform.eel.EelExecApi$InteractionOptions
- getScope():kotlinx.coroutines.CoroutineScope
- *:getWorkingDirectory():com.intellij.platform.eel.path.EelPath
*:com.intellij.platform.eel.EelExecApi$InteractionOptions
*f:com.intellij.platform.eel.EelExecApi$Pty
@@ -75,6 +76,7 @@
- *f:env(java.util.Map):com.intellij.platform.eel.EelExecApiHelpers$SpawnProcess
- *f:exe(java.lang.String):com.intellij.platform.eel.EelExecApiHelpers$SpawnProcess
- *f:interactionOptions(com.intellij.platform.eel.EelExecApi$InteractionOptions):com.intellij.platform.eel.EelExecApiHelpers$SpawnProcess
- f:scope(kotlinx.coroutines.CoroutineScope):com.intellij.platform.eel.EelExecApiHelpers$SpawnProcess
- *f:workingDirectory(com.intellij.platform.eel.path.EelPath):com.intellij.platform.eel.EelExecApiHelpers$SpawnProcess
f:com.intellij.platform.eel.EelExecApiHelpersKt
- *sf:spawnProcess(com.intellij.platform.eel.EelExecApi,java.lang.String):com.intellij.platform.eel.EelExecApiHelpers$SpawnProcess
@@ -96,6 +98,7 @@ f:com.intellij.platform.eel.EelExecApiKt
- *f:env(java.util.Map):com.intellij.platform.eel.EelExecPosixApiHelpers$SpawnProcess
- *f:exe(java.lang.String):com.intellij.platform.eel.EelExecPosixApiHelpers$SpawnProcess
- *f:interactionOptions(com.intellij.platform.eel.EelExecApi$InteractionOptions):com.intellij.platform.eel.EelExecPosixApiHelpers$SpawnProcess
- f:scope(kotlinx.coroutines.CoroutineScope):com.intellij.platform.eel.EelExecPosixApiHelpers$SpawnProcess
- *f:workingDirectory(com.intellij.platform.eel.path.EelPath):com.intellij.platform.eel.EelExecPosixApiHelpers$SpawnProcess
f:com.intellij.platform.eel.EelExecPosixApiHelpersKt
- *sf:spawnProcess(com.intellij.platform.eel.EelExecPosixApi,java.lang.String):com.intellij.platform.eel.EelExecPosixApiHelpers$SpawnProcess
@@ -112,6 +115,7 @@ f:com.intellij.platform.eel.EelExecPosixApiHelpersKt
- *f:env(java.util.Map):com.intellij.platform.eel.EelExecWindowsApiHelpers$SpawnProcess
- *f:exe(java.lang.String):com.intellij.platform.eel.EelExecWindowsApiHelpers$SpawnProcess
- *f:interactionOptions(com.intellij.platform.eel.EelExecApi$InteractionOptions):com.intellij.platform.eel.EelExecWindowsApiHelpers$SpawnProcess
- f:scope(kotlinx.coroutines.CoroutineScope):com.intellij.platform.eel.EelExecWindowsApiHelpers$SpawnProcess
- *f:workingDirectory(com.intellij.platform.eel.path.EelPath):com.intellij.platform.eel.EelExecWindowsApiHelpers$SpawnProcess
f:com.intellij.platform.eel.EelExecWindowsApiHelpersKt
- *sf:spawnProcess(com.intellij.platform.eel.EelExecWindowsApi,java.lang.String):com.intellij.platform.eel.EelExecWindowsApiHelpers$SpawnProcess
@@ -338,6 +342,7 @@ f:com.intellij.platform.eel.EelTunnelsApiKt
- *f:env(java.util.Map):com.intellij.platform.eel.ExecuteProcessOptionsBuilder
- *f:exe(java.lang.String):com.intellij.platform.eel.ExecuteProcessOptionsBuilder
- *f:interactionOptions(com.intellij.platform.eel.EelExecApi$InteractionOptions):com.intellij.platform.eel.ExecuteProcessOptionsBuilder
- f:scope(kotlinx.coroutines.CoroutineScope):com.intellij.platform.eel.ExecuteProcessOptionsBuilder
- *f:workingDirectory(com.intellij.platform.eel.path.EelPath):com.intellij.platform.eel.ExecuteProcessOptionsBuilder
*f:com.intellij.platform.eel.GetConnectionToRemotePortArgsBuilder
- <init>():V
@@ -9,6 +9,7 @@ import com.intellij.platform.eel.EelExecApi.ExecuteProcessOptions
import com.intellij.platform.eel.EelExecApi.InteractionOptions
import com.intellij.platform.eel.EelExecApi.PtyOrStdErrSettings
import com.intellij.platform.eel.path.EelPath
import kotlinx.coroutines.CoroutineScope
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.CheckReturnValue
@@ -76,6 +77,8 @@ object EelExecApiHelpers {
private var ptyOrStdErrSettings: PtyOrStdErrSettings? = interactionOptions
private var scope: CoroutineScope? = null
private var workingDirectory: EelPath? = null
@ApiStatus.Experimental
@@ -127,6 +130,13 @@ object EelExecApiHelpers {
this.ptyOrStdErrSettings = arg
}
/**
* Scope this process is bound to. Once scope dies -- this process dies as well.
*/
fun scope(arg: CoroutineScope?): Execute = apply {
this.scope = arg
}
/**
* All argument, all paths, should be valid for the remote machine. F.i., if the IDE runs on Windows, but IJent runs on Linux,
* [ExecuteProcessOptions.workingDirectory] is the path on the Linux host. There's no automatic path mapping in this interface.
@@ -149,6 +159,7 @@ object EelExecApiHelpers {
exe = exe,
interactionOptions = interactionOptions,
ptyOrStdErrSettings = ptyOrStdErrSettings,
scope = scope,
workingDirectory = workingDirectory,
)
)
@@ -171,6 +182,8 @@ object EelExecApiHelpers {
private var ptyOrStdErrSettings: PtyOrStdErrSettings? = interactionOptions
private var scope: CoroutineScope? = null
private var workingDirectory: EelPath? = null
@ApiStatus.Experimental
@@ -222,6 +235,13 @@ object EelExecApiHelpers {
this.ptyOrStdErrSettings = arg
}
/**
* Scope this process is bound to. Once scope dies -- this process dies as well.
*/
fun scope(arg: CoroutineScope?): SpawnProcess = apply {
this.scope = arg
}
/**
* All argument, all paths, should be valid for the remote machine. F.i., if the IDE runs on Windows, but IJent runs on Linux,
* [ExecuteProcessOptions.workingDirectory] is the path on the Linux host. There's no automatic path mapping in this interface.
@@ -245,6 +265,7 @@ object EelExecApiHelpers {
exe = exe,
interactionOptions = interactionOptions,
ptyOrStdErrSettings = ptyOrStdErrSettings,
scope = scope,
workingDirectory = workingDirectory,
)
)
@@ -9,6 +9,7 @@ import com.intellij.platform.eel.EelExecApi.ExecuteProcessOptions
import com.intellij.platform.eel.EelExecApi.InteractionOptions
import com.intellij.platform.eel.EelExecApi.PtyOrStdErrSettings
import com.intellij.platform.eel.path.EelPath
import kotlinx.coroutines.CoroutineScope
import org.jetbrains.annotations.ApiStatus
@@ -48,6 +49,8 @@ object EelExecPosixApiHelpers {
private var ptyOrStdErrSettings: PtyOrStdErrSettings? = interactionOptions
private var scope: CoroutineScope? = null
private var workingDirectory: EelPath? = null
@ApiStatus.Experimental
@@ -99,6 +102,13 @@ object EelExecPosixApiHelpers {
this.ptyOrStdErrSettings = arg
}
/**
* Scope this process is bound to. Once scope dies -- this process dies as well.
*/
fun scope(arg: CoroutineScope?): SpawnProcess = apply {
this.scope = arg
}
/**
* All argument, all paths, should be valid for the remote machine. F.i., if the IDE runs on Windows, but IJent runs on Linux,
* [ExecuteProcessOptions.workingDirectory] is the path on the Linux host. There's no automatic path mapping in this interface.
@@ -121,6 +131,7 @@ object EelExecPosixApiHelpers {
exe = exe,
interactionOptions = interactionOptions,
ptyOrStdErrSettings = ptyOrStdErrSettings,
scope = scope,
workingDirectory = workingDirectory,
)
)
@@ -9,6 +9,7 @@ import com.intellij.platform.eel.EelExecApi.ExecuteProcessOptions
import com.intellij.platform.eel.EelExecApi.InteractionOptions
import com.intellij.platform.eel.EelExecApi.PtyOrStdErrSettings
import com.intellij.platform.eel.path.EelPath
import kotlinx.coroutines.CoroutineScope
import org.jetbrains.annotations.ApiStatus
@@ -48,6 +49,8 @@ object EelExecWindowsApiHelpers {
private var ptyOrStdErrSettings: PtyOrStdErrSettings? = interactionOptions
private var scope: CoroutineScope? = null
private var workingDirectory: EelPath? = null
@ApiStatus.Experimental
@@ -99,6 +102,13 @@ object EelExecWindowsApiHelpers {
this.ptyOrStdErrSettings = arg
}
/**
* Scope this process is bound to. Once scope dies -- this process dies as well.
*/
fun scope(arg: CoroutineScope?): SpawnProcess = apply {
this.scope = arg
}
/**
* All argument, all paths, should be valid for the remote machine. F.i., if the IDE runs on Windows, but IJent runs on Linux,
* [ExecuteProcessOptions.workingDirectory] is the path on the Linux host. There's no automatic path mapping in this interface.
@@ -121,6 +131,7 @@ object EelExecWindowsApiHelpers {
exe = exe,
interactionOptions = interactionOptions,
ptyOrStdErrSettings = ptyOrStdErrSettings,
scope = scope,
workingDirectory = workingDirectory,
)
)
@@ -8,6 +8,7 @@ import com.intellij.platform.eel.EelExecApi.ExecuteProcessOptions
import com.intellij.platform.eel.EelExecApi.InteractionOptions
import com.intellij.platform.eel.EelExecApi.PtyOrStdErrSettings
import com.intellij.platform.eel.path.EelPath
import kotlinx.coroutines.CoroutineScope
import org.jetbrains.annotations.ApiStatus
@@ -31,6 +32,8 @@ class ExecuteProcessOptionsBuilder(
private var ptyOrStdErrSettings: PtyOrStdErrSettings? = interactionOptions
private var scope: CoroutineScope? = null
private var workingDirectory: EelPath? = null
@ApiStatus.Experimental
@@ -82,6 +85,13 @@ class ExecuteProcessOptionsBuilder(
this.ptyOrStdErrSettings = arg
}
/**
* Scope this process is bound to. Once scope dies -- this process dies as well.
*/
fun scope(arg: CoroutineScope?): ExecuteProcessOptionsBuilder = apply {
this.scope = arg
}
/**
* All argument, all paths, should be valid for the remote machine. F.i., if the IDE runs on Windows, but IJent runs on Linux,
* [ExecuteProcessOptions.workingDirectory] is the path on the Linux host. There's no automatic path mapping in this interface.
@@ -98,6 +108,7 @@ class ExecuteProcessOptionsBuilder(
exe = exe,
interactionOptions = interactionOptions,
ptyOrStdErrSettings = ptyOrStdErrSettings,
scope = scope,
workingDirectory = workingDirectory,
)
}
@@ -109,5 +120,6 @@ internal class ExecuteProcessOptionsImpl(
override val exe: String,
override val interactionOptions: InteractionOptions?,
override val ptyOrStdErrSettings: PtyOrStdErrSettings?,
override val scope: CoroutineScope?,
override val workingDirectory: EelPath?,
) : ExecuteProcessOptions
@@ -5,6 +5,7 @@ import com.intellij.platform.eel.EelExecApi.ExecuteProcessOptions
import com.intellij.platform.eel.channels.EelReceiveChannel
import com.intellij.platform.eel.channels.EelSendChannel
import com.intellij.platform.eel.path.EelPath
import kotlinx.coroutines.CoroutineScope
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.CheckReturnValue
@@ -50,6 +51,11 @@ sealed interface EelExecApi {
@get:ApiStatus.Experimental
val args: List<String> get() = listOf()
/**
* Scope this process is bound to. Once scope dies -- this process dies as well.
*/
val scope: CoroutineScope? get() = null
/**
* By default, environment is always inherited, which may be unwanted. [ExecuteProcessOptions.env] allows
* to alter some environment variables, it doesn't clear the variables from the parent. When the process should be started in an