From 9cf4ff7e6ea0fbd2d5afd9d7e630a9f8a90c8885 Mon Sep 17 00:00:00 2001 From: "Ilya.Kazakevich" Date: Fri, 4 Jul 2025 20:22:01 +0200 Subject: [PATCH] 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 --- .../eel/impl/local/EelLocalExecApi.kt | 9 +++- .../eel/provider/utils/processScopeUtil.kt | 48 +++++++++++++++++++ platform/eel/api-dump-experimental.txt | 5 ++ .../platform/eel/EelExecApiHelpers.kt | 21 ++++++++ .../platform/eel/EelExecPosixApiHelpers.kt | 11 +++++ .../platform/eel/EelExecWindowsApiHelpers.kt | 11 +++++ .../eel/ExecuteProcessOptionsBuilder.kt | 12 +++++ .../com/intellij/platform/eel/EelExecApi.kt | 6 +++ 8 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 platform/eel-provider/src/com/intellij/platform/eel/provider/utils/processScopeUtil.kt diff --git a/platform/eel-impl/src/com/intellij/platform/eel/impl/local/EelLocalExecApi.kt b/platform/eel-impl/src/com/intellij/platform/eel/impl/local/EelLocalExecApi.kt index cac3900b3589..f0a844f61a5d 100644 --- a/platform/eel-impl/src/com/intellij/platform/eel/impl/local/EelLocalExecApi.kt +++ b/platform/eel-impl/src/com/intellij/platform/eel/impl/local/EelLocalExecApi.kt @@ -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 diff --git a/platform/eel-provider/src/com/intellij/platform/eel/provider/utils/processScopeUtil.kt b/platform/eel-provider/src/com/intellij/platform/eel/provider/utils/processScopeUtil.kt new file mode 100644 index 000000000000..933084647a5a --- /dev/null +++ b/platform/eel-provider/src/com/intellij/platform/eel/provider/utils/processScopeUtil.kt @@ -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") + } +} \ No newline at end of file diff --git a/platform/eel/api-dump-experimental.txt b/platform/eel/api-dump-experimental.txt index dcbaaee08335..2568651ea035 100644 --- a/platform/eel/api-dump-experimental.txt +++ b/platform/eel/api-dump-experimental.txt @@ -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 - ():V diff --git a/platform/eel/gen-builders/com/intellij/platform/eel/EelExecApiHelpers.kt b/platform/eel/gen-builders/com/intellij/platform/eel/EelExecApiHelpers.kt index 1cd7b9fe4bba..f7ac89dcd50f 100644 --- a/platform/eel/gen-builders/com/intellij/platform/eel/EelExecApiHelpers.kt +++ b/platform/eel/gen-builders/com/intellij/platform/eel/EelExecApiHelpers.kt @@ -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, ) ) diff --git a/platform/eel/gen-builders/com/intellij/platform/eel/EelExecPosixApiHelpers.kt b/platform/eel/gen-builders/com/intellij/platform/eel/EelExecPosixApiHelpers.kt index 5901f223491c..64e2e0012df4 100644 --- a/platform/eel/gen-builders/com/intellij/platform/eel/EelExecPosixApiHelpers.kt +++ b/platform/eel/gen-builders/com/intellij/platform/eel/EelExecPosixApiHelpers.kt @@ -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, ) ) diff --git a/platform/eel/gen-builders/com/intellij/platform/eel/EelExecWindowsApiHelpers.kt b/platform/eel/gen-builders/com/intellij/platform/eel/EelExecWindowsApiHelpers.kt index 668b72c1c137..5580ddbf043b 100644 --- a/platform/eel/gen-builders/com/intellij/platform/eel/EelExecWindowsApiHelpers.kt +++ b/platform/eel/gen-builders/com/intellij/platform/eel/EelExecWindowsApiHelpers.kt @@ -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, ) ) diff --git a/platform/eel/gen-builders/com/intellij/platform/eel/ExecuteProcessOptionsBuilder.kt b/platform/eel/gen-builders/com/intellij/platform/eel/ExecuteProcessOptionsBuilder.kt index 70ce34c48894..1405c5bb94f2 100644 --- a/platform/eel/gen-builders/com/intellij/platform/eel/ExecuteProcessOptionsBuilder.kt +++ b/platform/eel/gen-builders/com/intellij/platform/eel/ExecuteProcessOptionsBuilder.kt @@ -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 \ No newline at end of file diff --git a/platform/eel/src/com/intellij/platform/eel/EelExecApi.kt b/platform/eel/src/com/intellij/platform/eel/EelExecApi.kt index 054a4a791afb..7ea766cbfbb8 100644 --- a/platform/eel/src/com/intellij/platform/eel/EelExecApi.kt +++ b/platform/eel/src/com/intellij/platform/eel/EelExecApi.kt @@ -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 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