Module EEL: API for local and non-local processes.

See `com.intellij.platform.eel` package info

GitOrigin-RevId: 11d0a8b0d9f75274bf604518dbe09e2ac85def20
This commit is contained in:
Ilya.Kazakevich
2024-08-31 04:22:06 +02:00
committed by intellij-monorepo-bot
parent 71f50958a2
commit bc2a7e6a45
30 changed files with 172 additions and 720 deletions

View File

@@ -9,5 +9,7 @@
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="library" name="jetbrains-annotations" level="project" />
<orderEntry type="library" name="kotlinx-coroutines-core" level="project" />
<orderEntry type="module" module-name="intellij.platform.util.base" />
</component>
</module>

View File

@@ -1,57 +1,23 @@
// 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
package com.intellij.platform.eel
import com.intellij.platform.ijent.fs.IjentFileSystemApi
import com.intellij.platform.ijent.fs.IjentFileSystemPosixApi
import com.intellij.platform.ijent.fs.IjentFileSystemWindowsApi
/**
* Provides access to an IJent process running on some machine. An instance of this interface gives ability to run commands
* on a local or a remote machine. Every instance corresponds to a single machine, i.e. unlike Run Targets, if IJent is launched
* in a Docker container, every call to execute a process (see [IjentExecApi]) runs a command in the same Docker container.
* Provides access to a local machine or an IJent process running on some machine. An instance of this interface gives ability to run commands
* on a local or a remote machine.
* in a Docker container, every call to execute a process (see [EelExecApi]) runs a command in the same Docker container.
*
* Usually, [com.intellij.platform.ijent.deploy] creates instances of [IjentApi].
*/
sealed interface IjentApi : AutoCloseable {
val platform: IjentPlatform
interface EelApi {
val platform: EelPlatform
/**
* Checks if the API is active and is safe to use. If it returns false, IJent on the other side is certainly unavailable.
* If it returns true, it's likely available.
*
* The property must return true as soon as [close] is called.
*
* The property must not perform any blocking operation and must work fast.
*/
val isRunning: Boolean
/**
* Returns basic info about the process that doesn't change during the lifetime of the process.
*/
val info: IjentInfo
/** Docs: [EelExecApi] */
val exec: EelExecApi
/**
* Explicitly terminates the process on the remote machine.
*
* The method is not supposed to block the current thread.
*
* For awaiting, use [waitUntilExit].
*/
override fun close()
/**
* Suspends until the IJent process on the remote side terminates.
* This method doesn't throw exceptions.
*/
suspend fun waitUntilExit()
/** Docs: [IjentExecApi] */
val exec: IjentExecApi
val fs: IjentFileSystemApi
/** Docs: [IjentTunnelsApi] */
val tunnels: IjentTunnelsApi
/** Docs: [EelTunnelsApi] */
val tunnels: EelTunnelsApi
/**
* On Unix-like OS, PID is int32. On Windows, PID is uint32. The type of Long covers both PID types, and a separate class doesn't allow
@@ -62,14 +28,10 @@ sealed interface IjentApi : AutoCloseable {
}
}
interface IjentPosixApi : IjentApi {
override val info: IjentPosixInfo
override val fs: IjentFileSystemPosixApi
override val tunnels: IjentTunnelsPosixApi
interface EelPosixApi : EelApi {
override val tunnels: EelTunnelsPosixApi
}
interface IjentWindowsApi : IjentApi {
override val info: IjentWindowsInfo
override val fs: IjentFileSystemWindowsApi
override val tunnels: IjentTunnelsWindowsApi
interface EelWindowsApi : EelApi {
override val tunnels: EelTunnelsWindowsApi
}

View File

@@ -1,19 +1,19 @@
// 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
package com.intellij.platform.eel
/**
* Methods related to process execution: start a process, collect stdin/stdout/stderr of the process, etc.
*/
interface IjentExecApi {
interface EelExecApi {
// TODO Extract into a separate interface, like IjentFileSystemApi.Arguments
/**
* Starts a process on a remote machine. Right now, the child process may outlive the instance of IJent.
* Starts a process on a local or remote machine.
* stdin, stdout and stderr of the process are always forwarded, if there are.
*
* Beware that processes with [ExecuteProcessBuilder.pty] usually don't have stderr.
* The [IjentChildProcess.stderr] must be an empty stream in such case.
* The [EelProcess.stderr] must be an empty stream in such case.
*
* By default, environment is always inherited from the running IJent instance, which may be unwanted. [ExecuteProcessBuilder.env] allows
* By default, environment is always inherited, which may be unwanted. [ExecuteProcessBuilder.env] allows
* to alter some environment variables, it doesn't clear the variables from the parent. When the process should be started in an
* environment like in a terminal, the response of [fetchLoginShellEnvVariables] should be put into [ExecuteProcessBuilder.env].
*
@@ -30,7 +30,7 @@ interface IjentExecApi {
fun workingDirectory(workingDirectory: String?): ExecuteProcessBuilder
/**
* Executes the process, returning either an [IjentChildProcess] or an error provided by the remote operating system.
* Executes the process, returning either an [EelProcess] or an error provided by the remote operating system.
*
* The instance of the [ExecuteProcessBuilder] _may_ become invalid after this call.
*
@@ -42,11 +42,10 @@ interface IjentExecApi {
/**
* Gets the same environment variables on the remote machine as the user would get if they run the shell.
*/
@Throws(IjentUnavailableException::class)
suspend fun fetchLoginShellEnvVariables(): Map<String, String>
sealed interface ExecuteProcessResult {
class Success(val process: IjentChildProcess) : ExecuteProcessResult
class Success(val process: EelProcess) : ExecuteProcessResult
data class Failure(val errno: Int, val message: String) : ExecuteProcessResult
}
@@ -54,14 +53,13 @@ interface IjentExecApi {
data class Pty(val columns: Int, val rows: Int, val echo: Boolean)
}
/** Docs: [IjentExecApi.executeProcessBuilder] */
@Throws(IjentUnavailableException::class)
suspend fun IjentExecApi.executeProcess(exe: String, vararg args: String): IjentExecApi.ExecuteProcessResult =
/** Docs: [EelExecApi.executeProcessBuilder] */
suspend fun EelExecApi.executeProcess(exe: String, vararg args: String): EelExecApi.ExecuteProcessResult =
executeProcessBuilder(exe).args(listOf(*args)).execute()
/** Docs: [IjentExecApi.executeProcessBuilder] */
fun IjentExecApi.executeProcessBuilder(exe: String, arg1: String, vararg args: String): IjentExecApi.ExecuteProcessBuilder =
/** Docs: [EelExecApi.executeProcessBuilder] */
fun EelExecApi.executeProcessBuilder(exe: String, arg1: String, vararg args: String): EelExecApi.ExecuteProcessBuilder =
executeProcessBuilder(exe).args(listOf(arg1, *args))
fun IjentExecApi.ExecuteProcessBuilder.args(first: String, vararg other: String): IjentExecApi.ExecuteProcessBuilder =
fun EelExecApi.ExecuteProcessBuilder.args(first: String, vararg other: String): EelExecApi.ExecuteProcessBuilder =
args(listOf(first, *other))

View File

@@ -1,11 +1,11 @@
// 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
package com.intellij.platform.eel
sealed interface IjentPlatform {
sealed interface Posix : IjentPlatform
sealed interface EelPlatform {
sealed interface Posix : EelPlatform
sealed interface Linux : Posix
sealed interface Darwin : Posix
sealed interface Windows : IjentPlatform
sealed interface Windows : EelPlatform
data object Arm64Darwin : Darwin
data object Aarch64Linux : Linux
@@ -14,7 +14,7 @@ sealed interface IjentPlatform {
data object X64Windows : Windows
companion object {
fun getFor(os: String, arch: String): IjentPlatform? =
fun getFor(os: String, arch: String): EelPlatform? =
when (os.lowercase()) {
"darwin" -> when (arch.lowercase()) {
"arm64", "aarch64" -> Arm64Darwin
@@ -34,12 +34,3 @@ sealed interface IjentPlatform {
}
}
}
val IjentPlatform.executableName: String
get() = when (this) {
IjentPlatform.Arm64Darwin -> "ijent-aarch64-apple-darwin-release"
IjentPlatform.X8664Darwin -> "ijent-x86_64-apple-darwin-release"
IjentPlatform.Aarch64Linux -> "ijent-aarch64-unknown-linux-musl-release"
IjentPlatform.X8664Linux -> "ijent-x86_64-unknown-linux-musl-release"
IjentPlatform.X64Windows -> "ijent-x86_64-pc-windows-gnu-release.exe"
}

View File

@@ -1,18 +1,16 @@
// 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
package com.intellij.platform.eel
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel
/**
* Represents some process which was launched by IJent via [IjentApi.executeProcess].
* Represents some process which was launched via [EelExecApi.executeProcess].
*
* There are adapters for already written code: [com.intellij.execution.ijent.IjentChildProcessAdapter]
* and [com.intellij.execution.ijent.IjentChildPtyProcessAdapter].
*/
interface IjentChildProcess {
val pid: IjentApi.Pid
interface EelProcess {
val pid: EelApi.Pid
/**
* Although data transmission via this channel could potentially stall due to overflow of [kotlinx.coroutines.channels.Channel],
@@ -32,7 +30,7 @@ interface IjentChildProcess {
*
* Notice that every data chunk is flushed into the process separately. There's no buffering.
*/
@Throws(SendStdinError::class, IjentUnavailableException::class)
@Throws(SendStdinError::class)
suspend fun sendStdinWithConfirmation(data: ByteArray)
sealed class SendStdinError(msg: String) : Exception(msg) {
@@ -51,7 +49,6 @@ interface IjentChildProcess {
*
* Does nothing yet on Windows.
*/
@Throws(IjentUnavailableException::class)
suspend fun interrupt()
/**
@@ -59,7 +56,6 @@ interface IjentChildProcess {
*
* Calls [`ExitProcess`](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-exitprocess) on Windows.
*/
@Throws(IjentUnavailableException::class)
suspend fun terminate()
/**
@@ -68,10 +64,9 @@ interface IjentChildProcess {
* Calls [`TerminateProcess`](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-terminateprocess)
* on Windows.
*/
@Throws(IjentUnavailableException::class)
suspend fun kill()
@Throws(ResizePtyError::class, IjentUnavailableException::class) // Can't use @CheckReturnValue: KTIJ-7061
@Throws(ResizePtyError::class) // Can't use @CheckReturnValue: KTIJ-7061
suspend fun resizePty(columns: Int, rows: Int)
sealed class ResizePtyError(msg: String) : Exception(msg) {

View File

@@ -1,9 +1,9 @@
// 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
package com.intellij.platform.eel
import com.intellij.openapi.util.NlsSafe
import com.intellij.platform.ijent.IjentNetworkResult.Ok
import com.intellij.platform.ijent.IjentTunnelsApi.Connection
import com.intellij.platform.eel.EelNetworkResult.Ok
import com.intellij.platform.eel.EelTunnelsApi.Connection
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel
@@ -15,15 +15,15 @@ import kotlin.time.Duration
/**
* Methods for launching tunnels for TCP sockets, Unix sockets, etc.
*/
sealed interface IjentTunnelsApi {
interface EelTunnelsApi {
/**
* **This is a delicate API, for applied usages, please consider [withConnectionToRemotePort]**.
*
* Creates a connection to a TCP socket to a named host specified by [address].
*
* If the result is [IjentNetworkResult.Error], then there was an error during establishment of the connection.
* Otherwise, the result is [IjentNetworkResult.Ok], which means that the connection is ready to use.
* If the result is [EelNetworkResult.Error], then there was an error during establishment of the connection.
* Otherwise, the result is [EelNetworkResult.Ok], which means that the connection is ready to use.
*
* The connection exists as a pair of channels [Connection.channelToServer] and [Connection.channelFromServer],
* which allow communicating to a remote server from the IDE side.
@@ -40,8 +40,7 @@ sealed interface IjentTunnelsApi {
*
* One should not forget to invoke [Connection.close] when the connection is not needed.
*/
@Throws(IjentUnavailableException::class)
suspend fun getConnectionToRemotePort(address: HostAddress): IjentNetworkResult<Connection, IjentConnectionError>
suspend fun getConnectionToRemotePort(address: HostAddress): EelNetworkResult<Connection, EelConnectionError>
/**
* Creates a builder for address on the remote host.
@@ -87,8 +86,8 @@ sealed interface IjentTunnelsApi {
/**
* Sets timeout for connecting to remote host.
* If the connection could not be established before [timeout], then [IjentConnectionError.ConnectionTimeout] would be returned
* in [IjentTunnelsApi.getConnectionToRemotePort].
* If the connection could not be established before [timeout], then [EelConnectionError.ConnectionTimeout] would be returned
* in [EelTunnelsApi.getConnectionToRemotePort].
*
* Default value: 10 seconds.
* The recognizable granularity is milliseconds.
@@ -121,48 +120,41 @@ sealed interface IjentTunnelsApi {
* Sets the size of send buffer of the socket
* @see java.net.SocketOptions.SO_SNDBUF
*/
@Throws(IjentUnavailableException::class)
suspend fun setSendBufferSize(size: UInt)
/**
* Sets the receive buffer size of the socket
* @see java.net.SocketOptions.SO_RCVBUF
*/
@Throws(IjentUnavailableException::class)
suspend fun setReceiveBufferSize(size: UInt)
/**
* Sets the keep alive option for the socket
* @see java.net.SocketOptions.SO_KEEPALIVE
*/
@Throws(IjentUnavailableException::class)
suspend fun setKeepAlive(keepAlive: Boolean)
/**
* Sets the possibility to reuse address of the socket
* @see java.net.SocketOptions.SO_REUSEADDR
*/
@Throws(IjentUnavailableException::class)
suspend fun setReuseAddr(reuseAddr: Boolean)
/**
* Sets linger timeout for the socket
* @see java.net.SocketOptions.SO_LINGER
*/
@Throws(IjentUnavailableException::class)
suspend fun setLinger(lingerInterval: Duration)
/**
* Disables pending data until acknowledgement
* @see java.net.SocketOptions.TCP_NODELAY
*/
@Throws(IjentUnavailableException::class)
suspend fun setNoDelay(noDelay: Boolean)
/**
* Closes the connection to the socket.
*/
@Throws(IjentUnavailableException::class)
suspend fun close()
}
@@ -187,7 +179,7 @@ operator fun Connection.component1(): SendChannel<ByteBuffer> = channelToServer
*/
operator fun Connection.component2(): ReceiveChannel<ByteBuffer> = channelFromServer
interface IjentTunnelsPosixApi : IjentTunnelsApi {
interface EelTunnelsPosixApi : EelTunnelsApi {
/**
* Creates a remote UNIX socket forwarding. IJent listens for a connection on the remote machine, and when the connection
* is accepted, the IDE communicates to the remote client via a pair of Kotlin channels.
@@ -212,7 +204,6 @@ interface IjentTunnelsPosixApi : IjentTunnelsApi {
* }
* ```
*/
@Throws(IjentUnavailableException::class)
suspend fun listenOnUnixSocket(path: CreateFilePath = CreateFilePath.MkTemp()): ListenOnUnixSocketResult
data class ListenOnUnixSocketResult(
@@ -229,7 +220,7 @@ interface IjentTunnelsPosixApi : IjentTunnelsApi {
}
}
interface IjentTunnelsWindowsApi : IjentTunnelsApi
interface EelTunnelsWindowsApi : EelTunnelsApi
/**
* Convenience function for working with a connection to a remote server.
@@ -238,7 +229,7 @@ interface IjentTunnelsWindowsApi : IjentTunnelsApi
* ```kotlin
*
* suspend fun foo() {
* ijentTunnelsApi.withConnectionToRemotePort("localhost", 8080, {
* EelTunnelsApi.withConnectionToRemotePort("localhost", 8080, {
* myErrorReporter.report(it)
* }) { (channelTo, channelFrom) ->
* handleConnection(channelTo, channelFrom)
@@ -250,16 +241,15 @@ interface IjentTunnelsWindowsApi : IjentTunnelsApi
* If the connection could not be established, then [errorHandler] is invoked.
* Otherwise, [action] is invoked. The connection gets automatically closed when [action] finishes.
*
* @see com.intellij.platform.ijent.IjentTunnelsApi.getConnectionToRemotePort for more details on the behavior of [Connection]
* @see EelTunnelsApi.getConnectionToRemotePort for more details on the behavior of [Connection]
*/
@Throws(IjentUnavailableException::class)
suspend fun <T> IjentTunnelsApi.withConnectionToRemotePort(
hostAddress: IjentTunnelsApi.HostAddress,
errorHandler: suspend (IjentConnectionError) -> T,
suspend fun <T> EelTunnelsApi.withConnectionToRemotePort(
hostAddress: EelTunnelsApi.HostAddress,
errorHandler: suspend (EelConnectionError) -> T,
action: suspend CoroutineScope.(Connection) -> T,
): T =
when (val connectionResult = getConnectionToRemotePort(hostAddress)) {
is IjentNetworkResult.Error -> errorHandler(connectionResult.error)
is EelNetworkResult.Error -> errorHandler(connectionResult.error)
is Ok -> try {
coroutineScope { action(connectionResult.value) }
}
@@ -268,40 +258,38 @@ suspend fun <T> IjentTunnelsApi.withConnectionToRemotePort(
}
}
@Throws(IjentUnavailableException::class)
suspend fun <T> IjentTunnelsApi.withConnectionToRemotePort(
suspend fun <T> EelTunnelsApi.withConnectionToRemotePort(
host: String, port: UShort,
errorHandler: suspend (IjentConnectionError) -> T,
errorHandler: suspend (EelConnectionError) -> T,
action: suspend CoroutineScope.(Connection) -> T,
): T = withConnectionToRemotePort(hostAddressBuilder(port).hostname(host).build(), errorHandler, action)
@Throws(IjentUnavailableException::class)
suspend fun <T> IjentTunnelsApi.withConnectionToRemotePort(
suspend fun <T> EelTunnelsApi.withConnectionToRemotePort(
remotePort: UShort,
errorHandler: suspend (IjentConnectionError) -> T,
errorHandler: suspend (EelConnectionError) -> T,
action: suspend CoroutineScope.(Connection) -> T,
): T = withConnectionToRemotePort("localhost", remotePort, errorHandler, action)
/**
* Represents a common class for all network-related errors appearing during the interaction with IJent
* Represents a common class for all network-related errors appearing during the interaction with IJent or local process
*/
sealed interface IjentNetworkError
sealed interface EelNetworkError
/**
* Represents a result of a network operation
*/
sealed interface IjentNetworkResult<out T, out E : IjentNetworkError> {
sealed interface EelNetworkResult<out T, out E : EelNetworkError> {
/**
* Used when a network operation completed successfully
*/
interface Ok<out T> : IjentNetworkResult<T, Nothing> {
interface Ok<out T> : EelNetworkResult<T, Nothing> {
val value: T
}
/**
* Used when a network operation completed with an error
*/
interface Error<out E : IjentNetworkError> : IjentNetworkResult<Nothing, E> {
interface Error<out E : EelNetworkError> : EelNetworkResult<Nothing, E> {
val error: E
}
}
@@ -309,17 +297,17 @@ sealed interface IjentNetworkResult<out T, out E : IjentNetworkError> {
/**
* An error that can happen during the creation of a connection to a remote server
*/
interface IjentConnectionError : IjentNetworkError {
interface EelConnectionError : EelNetworkError {
val message: @NlsSafe String
data object ConnectionTimeout : IjentConnectionError {
data object ConnectionTimeout : EelConnectionError {
override val message: @NlsSafe String = "Connection could not be established because of timeout"
}
/**
* Returned when a hostname on the remote server was resolved to multiple different addresses.
*/
data object AmbiguousAddress : IjentConnectionError {
data object AmbiguousAddress : EelConnectionError {
override val message: String = "Hostname could not be resolved uniquely"
}
@@ -327,19 +315,19 @@ interface IjentConnectionError : IjentNetworkError {
* Returned when a socket could not be created because of an OS error.
*/
@JvmInline
value class SocketCreationFailure(override val message: @NlsSafe String) : IjentConnectionError
value class SocketCreationFailure(override val message: @NlsSafe String) : EelConnectionError
/**
* Returned when resolve of remote address failed during the creation of a socket.
*/
object HostUnreachable : IjentConnectionError {
object HostUnreachable : EelConnectionError {
override val message: @NlsSafe String = "Remote host is unreachable"
}
/**
* Returned when the remote server does not accept connections.
*/
object ConnectionRefused : IjentConnectionError {
object ConnectionRefused : EelConnectionError {
override val message: @NlsSafe String = "Connection was refused by remote server"
}
@@ -347,13 +335,13 @@ interface IjentConnectionError : IjentNetworkError {
* Returned when hostname could not be resolved.
*/
@JvmInline
value class ResolveFailure(override val message: @NlsSafe String) : IjentConnectionError
value class ResolveFailure(override val message: @NlsSafe String) : EelConnectionError
/**
* Unknown failure during a connection establishment
*/
@JvmInline
value class UnknownFailure(override val message: @NlsSafe String) : IjentConnectionError
value class UnknownFailure(override val message: @NlsSafe String) : EelConnectionError
}

View File

@@ -1,4 +1,7 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
/**
* EEL API provides the same API for local and non-local (ssh/wsl/etc) system
*/
@ApiStatus.Internal
package com.intellij.platform.eel;

View File

@@ -13,6 +13,8 @@ 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.EelExecApi
import com.intellij.platform.eel.executeProcess
import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.platform.ide.progress.TaskCancellation
import com.intellij.platform.ide.progress.withModalProgress
@@ -20,7 +22,6 @@ import com.intellij.platform.ijent.IjentExecApi
import com.intellij.platform.ijent.IjentMissingBinary
import com.intellij.platform.ijent.community.impl.nio.IjentNioFileSystemProvider
import com.intellij.platform.ijent.deploy
import com.intellij.platform.ijent.executeProcess
import com.intellij.platform.ijent.spi.IjentDeployingStrategy
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.consumeEach
@@ -76,8 +77,8 @@ abstract class AbstractIjentVerificationAction : DumbAwareAction() {
launch {
val process = when (val p = ijent.exec.executeProcess("uname", "-a")) {
is IjentExecApi.ExecuteProcessResult.Failure -> error(p)
is IjentExecApi.ExecuteProcessResult.Success -> p.process
is EelExecApi.ExecuteProcessResult.Failure -> error(p)
is EelExecApi.ExecuteProcessResult.Success -> p.process
}
val stdout = ByteArrayOutputStream()
process.stdout.consumeEach(stdout::write)

View File

@@ -21,5 +21,6 @@
<orderEntry type="module" module-name="intellij.platform.core.nio.fs" />
<orderEntry type="module" module-name="intellij.platform.diagnostic.telemetry" />
<orderEntry type="module" module-name="intellij.platform.ijent" />
<orderEntry type="module" module-name="intellij.platform.eel" />
</component>
</module>

View File

@@ -1,20 +1,20 @@
// 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.community.impl
import com.intellij.platform.ijent.IjentApi
import com.intellij.platform.eel.EelApi
import com.intellij.platform.ijent.IjentPosixInfo
import com.intellij.platform.ijent.IjentWindowsInfo
data class IjentPosixInfoImpl(
override val architecture: String,
override val remotePid: IjentApi.Pid,
override val remotePid: EelApi.Pid,
override val version: String,
override val user: IjentPosixInfo.User,
) : IjentPosixInfo
data class IjentWindowsInfoImpl(
override val architecture: String,
override val remotePid: IjentApi.Pid,
override val remotePid: EelApi.Pid,
override val version: String,
override val user: IjentWindowsInfo.User,
) : IjentWindowsInfo

View File

@@ -14,5 +14,6 @@
<orderEntry type="library" scope="TEST" name="kotlin-test-assertions-core-jvm" level="project" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.platform.util.coroutines" />
<orderEntry type="module" module-name="intellij.platform.eel" exported="" />
</component>
</module>

View File

@@ -1,45 +1,13 @@
// 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
sealed interface IjentPlatform {
sealed interface Posix : IjentPlatform
sealed interface Linux : Posix
sealed interface Darwin : Posix
sealed interface Windows : IjentPlatform
import com.intellij.platform.eel.EelPlatform
data object Arm64Darwin : Darwin
data object Aarch64Linux : Linux
data object X8664Darwin : Darwin
data object X8664Linux : Linux
data object X64Windows : Windows
companion object {
fun getFor(os: String, arch: String): IjentPlatform? =
when (os.lowercase()) {
"darwin" -> when (arch.lowercase()) {
"arm64", "aarch64" -> Arm64Darwin
"amd64", "x86_64", "x86-64" -> X8664Darwin
else -> null
}
"linux" -> when (arch.lowercase()) {
"arm64", "aarch64" -> Aarch64Linux
"amd64", "x86_64", "x86-64" -> X8664Linux
else -> null
}
"windows" -> when (arch.lowercase()) {
"amd64", "x86_64", "x86-64" -> X64Windows
else -> null
}
else -> null
}
}
}
val IjentPlatform.executableName: String
val EelPlatform.executableName: String
get() = when (this) {
IjentPlatform.Arm64Darwin -> "ijent-aarch64-apple-darwin-release"
IjentPlatform.X8664Darwin -> "ijent-x86_64-apple-darwin-release"
IjentPlatform.Aarch64Linux -> "ijent-aarch64-unknown-linux-musl-release"
IjentPlatform.X8664Linux -> "ijent-x86_64-unknown-linux-musl-release"
IjentPlatform.X64Windows -> "ijent-x86_64-pc-windows-gnu-release.exe"
EelPlatform.Arm64Darwin -> "ijent-aarch64-apple-darwin-release"
EelPlatform.X8664Darwin -> "ijent-x86_64-apple-darwin-release"
EelPlatform.Aarch64Linux -> "ijent-aarch64-unknown-linux-musl-release"
EelPlatform.X8664Linux -> "ijent-x86_64-unknown-linux-musl-release"
EelPlatform.X64Windows -> "ijent-x86_64-pc-windows-gnu-release.exe"
}

View File

@@ -1,6 +1,10 @@
// 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
import com.intellij.platform.eel.EelPlatform
import com.intellij.platform.eel.EelPosixApi
import com.intellij.platform.eel.EelTunnelsApi
import com.intellij.platform.eel.EelWindowsApi
import com.intellij.platform.ijent.fs.IjentFileSystemApi
import com.intellij.platform.ijent.fs.IjentFileSystemPosixApi
import com.intellij.platform.ijent.fs.IjentFileSystemWindowsApi
@@ -8,12 +12,11 @@ import com.intellij.platform.ijent.fs.IjentFileSystemWindowsApi
/**
* Provides access to an IJent process running on some machine. An instance of this interface gives ability to run commands
* on a local or a remote machine. Every instance corresponds to a single machine, i.e. unlike Run Targets, if IJent is launched
* in a Docker container, every call to execute a process (see [IjentExecApi]) runs a command in the same Docker container.
* in a Docker container, every call to execute a process (see [com.intellij.platform.eel.EelExecApi]) runs a command in the same Docker container.
*
* Usually, [com.intellij.platform.ijent.deploy] creates instances of [IjentApi].
* Usually, [com.intellij.platform.ijent.deploy] creates instances of [com.intellij.platform.eel.IjentApi].
*/
sealed interface IjentApi : AutoCloseable {
val platform: IjentPlatform
/**
* Checks if the API is active and is safe to use. If it returns false, IJent on the other side is certainly unavailable.
@@ -45,30 +48,19 @@ sealed interface IjentApi : AutoCloseable {
*/
suspend fun waitUntilExit()
/** Docs: [IjentExecApi] */
/** Docs: [com.intellij.platform.eel.EelExecApi] */
val exec: IjentExecApi
val fs: IjentFileSystemApi
/** Docs: [IjentTunnelsApi] */
val tunnels: IjentTunnelsApi
/**
* On Unix-like OS, PID is int32. On Windows, PID is uint32. The type of Long covers both PID types, and a separate class doesn't allow
* to forget that fact and misuse types in APIs.
*/
interface Pid {
val value: Long
}
}
interface IjentPosixApi : IjentApi {
interface IjentPosixApi : IjentApi, EelPosixApi {
override val info: IjentPosixInfo
override val fs: IjentFileSystemPosixApi
override val tunnels: IjentTunnelsPosixApi
}
interface IjentWindowsApi : IjentApi {
interface IjentWindowsApi : IjentApi, EelWindowsApi {
override val info: IjentWindowsInfo
override val fs: IjentFileSystemWindowsApi
override val tunnels: IjentTunnelsWindowsApi

View File

@@ -1,82 +1,12 @@
// 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
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel
import com.intellij.platform.eel.EelProcess
/**
* Represents some process which was launched by IJent via [IjentApi.executeProcess].
* Represents some process which was launched by IJent via [com.intellij.platform.eel.EelApi.executeProcess].
*
* There are adapters for already written code: [com.intellij.execution.ijent.IjentChildProcessAdapter]
* and [com.intellij.execution.ijent.IjentChildPtyProcessAdapter].
*/
interface IjentChildProcess {
val pid: IjentApi.Pid
/**
* Although data transmission via this channel could potentially stall due to overflow of [kotlinx.coroutines.channels.Channel],
* this method does not allow to ensure that a data chunk was actually delivered to the remote process.
* For synchronous delivery that reports about delivery result, please use [sendStdinWithConfirmation].
*
* Note that each chunk of data is individually and immediately flushed into the process without any intermediate buffer storage.
*/
val stdin: SendChannel<ByteArray>
val stdout: ReceiveChannel<ByteArray>
val stderr: ReceiveChannel<ByteArray>
val exitCode: Deferred<Int>
/**
* Sends [data] into the process stdin and waits until the data is received by the process.
*
* Notice that every data chunk is flushed into the process separately. There's no buffering.
*/
@Throws(SendStdinError::class, IjentUnavailableException::class)
suspend fun sendStdinWithConfirmation(data: ByteArray)
sealed class SendStdinError(msg: String) : Exception(msg) {
class ProcessExited : SendStdinError("Process exited")
/**
* This error doesn't imply that the process has already exited. It is possible to close the stdin of the process, and the process
* may live quite a long time after that. However, usually processes exit immediately right after their stdin are closed.
* Therefore, it may turn out that the process exits at the moment when this error is being observed by the API user.
*/
class StdinClosed : SendStdinError("Stdin closed")
}
/**
* Sends `SIGINT` on Unix.
*
* Does nothing yet on Windows.
*/
@Throws(IjentUnavailableException::class)
suspend fun interrupt()
/**
* Sends `SIGTERM` on Unix.
*
* Calls [`ExitProcess`](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-exitprocess) on Windows.
*/
@Throws(IjentUnavailableException::class)
suspend fun terminate()
/**
* Sends `SIGKILL` on Unix.
*
* Calls [`TerminateProcess`](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-terminateprocess)
* on Windows.
*/
@Throws(IjentUnavailableException::class)
suspend fun kill()
@Throws(ResizePtyError::class, IjentUnavailableException::class) // Can't use @CheckReturnValue: KTIJ-7061
suspend fun resizePty(columns: Int, rows: Int)
sealed class ResizePtyError(msg: String) : Exception(msg) {
class ProcessExited : ResizePtyError("Process exited")
class NoPty : ResizePtyError("Process has no PTY")
data class Errno(val errno: Int, override val message: String) : ResizePtyError("[$errno] $message")
}
}
interface IjentChildProcess : EelProcess

View File

@@ -14,7 +14,7 @@ import kotlinx.coroutines.job
* [ijentName] is a label used for logging, debugging, thread names, etc.
*
* By default, the IJent executable exits when the IDE exits.
* [IjentApi.close] and [com.intellij.platform.ijent.bindToScope] may be used to terminate IJent earlier.
* [com.intellij.platform.eel.IjentApi.close] and [com.intellij.platform.ijent.bindToScope] may be used to terminate IJent earlier.
*
* TODO Either define thrown exceptions or return something like Result.
*/

View File

@@ -1,10 +1,8 @@
// 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
/**
* Methods related to process execution: start a process, collect stdin/stdout/stderr of the process, etc.
*/
interface IjentExecApi {
import com.intellij.platform.eel.EelExecApi
// TODO Extract into a separate interface, like IjentFileSystemApi.Arguments
/**
* Starts a process on a remote machine. Right now, the child process may outlive the instance of IJent.
@@ -20,48 +18,4 @@ interface IjentExecApi {
* All argument, all paths, should be valid for the remote machine. F.i., if the IDE runs on Windows, but IJent runs on Linux,
* [ExecuteProcessBuilder.workingDirectory] is the path on the Linux host. There's no automatic path mapping in this interface.
*/
fun executeProcessBuilder(exe: String): ExecuteProcessBuilder
/** Docs: [executeProcessBuilder] */
interface ExecuteProcessBuilder {
fun args(args: List<String>): ExecuteProcessBuilder
fun env(env: Map<String, String>): ExecuteProcessBuilder
fun pty(pty: Pty?): ExecuteProcessBuilder
fun workingDirectory(workingDirectory: String?): ExecuteProcessBuilder
/**
* Executes the process, returning either an [IjentChildProcess] or an error provided by the remote operating system.
*
* The instance of the [ExecuteProcessBuilder] _may_ become invalid after this call.
*
* The method may throw a RuntimeException only in critical cases like connection loss or a bug.
*/
suspend fun execute(): ExecuteProcessResult
}
/**
* Gets the same environment variables on the remote machine as the user would get if they run the shell.
*/
@Throws(IjentUnavailableException::class)
suspend fun fetchLoginShellEnvVariables(): Map<String, String>
sealed interface ExecuteProcessResult {
class Success(val process: IjentChildProcess) : ExecuteProcessResult
data class Failure(val errno: Int, val message: String) : ExecuteProcessResult
}
/** [echo] must be true in general and must be false when the user is asked for a password. */
data class Pty(val columns: Int, val rows: Int, val echo: Boolean)
}
/** Docs: [IjentExecApi.executeProcessBuilder] */
@Throws(IjentUnavailableException::class)
suspend fun IjentExecApi.executeProcess(exe: String, vararg args: String): IjentExecApi.ExecuteProcessResult =
executeProcessBuilder(exe).args(listOf(*args)).execute()
/** Docs: [IjentExecApi.executeProcessBuilder] */
fun IjentExecApi.executeProcessBuilder(exe: String, arg1: String, vararg args: String): IjentExecApi.ExecuteProcessBuilder =
executeProcessBuilder(exe).args(listOf(arg1, *args))
fun IjentExecApi.ExecuteProcessBuilder.args(first: String, vararg other: String): IjentExecApi.ExecuteProcessBuilder =
args(listOf(first, *other))
interface IjentExecApi: EelExecApi

View File

@@ -2,6 +2,7 @@
package com.intellij.platform.ijent
import com.intellij.openapi.components.serviceAsync
import com.intellij.platform.eel.EelPlatform
import org.jetbrains.annotations.ApiStatus.Internal
import java.nio.file.Path
@@ -18,14 +19,14 @@ interface IjentExecFileProvider {
* Gets the path to the IJent binary. Suggests to install the plugin via dialog windows, so the method may work unpredictably long.
*/
@Throws(IjentMissingBinary::class)
suspend fun getIjentBinary(targetPlatform: IjentPlatform): Path
suspend fun getIjentBinary(targetPlatform: EelPlatform): Path
}
class IjentMissingBinary(platform: IjentPlatform) : Exception("Failed to get an IJent binary for $platform") {
class IjentMissingBinary(platform: EelPlatform) : Exception("Failed to get an IJent binary for $platform") {
override fun getLocalizedMessage(): String = IjentBundle.message("failed.to.get.ijent.binary")
}
internal class DefaultIjentExecFileProvider : IjentExecFileProvider {
override suspend fun getIjentBinary(targetPlatform: IjentPlatform): Nothing =
override suspend fun getIjentBinary(targetPlatform: EelPlatform): Nothing =
throw IjentMissingBinary(targetPlatform)
}

View File

@@ -1,6 +1,8 @@
// 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
import com.intellij.platform.eel.EelApi
/**
* [architecture] is the remote architecture of the built binary. Intended to be used for debugging purposes.
* [remotePid] is a process ID of IJent running on the remote machine.
@@ -8,7 +10,7 @@ package com.intellij.platform.ijent
*/
sealed interface IjentInfo {
val architecture: String
val remotePid: IjentApi.Pid
val remotePid: EelApi.Pid
val version: String
val user: User

View File

@@ -1,359 +1,14 @@
// 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
import com.intellij.openapi.util.NlsSafe
import com.intellij.platform.ijent.IjentNetworkResult.Ok
import com.intellij.platform.ijent.IjentTunnelsApi.Connection
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.coroutineScope
import java.io.IOException
import java.nio.ByteBuffer
import kotlin.time.Duration
import com.intellij.platform.eel.EelTunnelsPosixApi
import com.intellij.platform.eel.EelTunnelsWindowsApi
/**
* Methods for launching tunnels for TCP sockets, Unix sockets, etc.
*/
sealed interface IjentTunnelsApi {
/**
* **This is a delicate API, for applied usages, please consider [withConnectionToRemotePort]**.
*
* Creates a connection to a TCP socket to a named host specified by [address].
*
* If the result is [IjentNetworkResult.Error], then there was an error during establishment of the connection.
* Otherwise, the result is [IjentNetworkResult.Ok], which means that the connection is ready to use.
*
* The connection exists as a pair of channels [Connection.channelToServer] and [Connection.channelFromServer],
* which allow communicating to a remote server from the IDE side.
*
* Packets sent to the channel and received from the channel may be split and/or concatenated.
* The packets may be split only if their size exceeds [com.intellij.platform.ijent.spi.RECOMMENDED_MAX_PACKET_SIZE].
*
* If the connection gets closed from the server, then the channels also get closed in the sense of [SendChannel.close].
*
* If an exception happens during sending, then [Connection.channelFromServer] gets closed exceptionally with [RemoteNetworkException].
*
* [Connection.channelToServer] can be closed separately with [SendChannel.close]. In this case, the EOF is sent to the server.
* Note, that [Connection.channelFromServer] is __not__ closed in this case.
*
* One should not forget to invoke [Connection.close] when the connection is not needed.
*/
@Throws(IjentUnavailableException::class)
suspend fun getConnectionToRemotePort(address: HostAddress): IjentNetworkResult<Connection, IjentConnectionError>
/**
* Creates a builder for address on the remote host.
*/
fun hostAddressBuilder(port: UShort): HostAddress.Builder
/**
* Represents an address to a remote host.
*/
interface HostAddress {
/**
* A builder class for remote host address.
*/
interface Builder {
/**
* Sets the hostname for a remote host.
* The hostname will be resolved remotely.
*
* By default, the hostname is `localhost`
*/
fun hostname(hostname: String): Builder
/**
* If [hostname] is resolved to an IPv4 address, then it is used.
*
* Overrides [preferIPv6] and [preferOSDefault]
*/
fun preferIPv4(): Builder
/**
* If [hostname] is resolved to an IPv6 address, then it is used.
* Overrides [preferIPv4] and [preferOSDefault]
*/
fun preferIPv6(): Builder
/**
* [hostname] is resolved according to the settings on the remote host.
*
* Overrides [preferIPv4] and [preferIPv6]. This is the default option.
*/
fun preferOSDefault(): Builder
/**
* Sets timeout for connecting to remote host.
* If the connection could not be established before [timeout], then [IjentConnectionError.ConnectionTimeout] would be returned
* in [IjentTunnelsApi.getConnectionToRemotePort].
*
* Default value: 10 seconds.
* The recognizable granularity is milliseconds.
*/
fun connectionTimeout(timeout: Duration): Builder
/**
* Builds a remote host address object.
*/
fun build(): HostAddress
}
}
/**
* Represents a controller for a remote connection
*/
interface Connection {
/**
* A channel to the remote server
*/
val channelToServer: SendChannel<ByteBuffer>
/**
* A channel from the remote server
*/
val channelFromServer: ReceiveChannel<ByteBuffer>
/**
* Sets the size of send buffer of the socket
* @see java.net.SocketOptions.SO_SNDBUF
*/
@Throws(IjentUnavailableException::class)
suspend fun setSendBufferSize(size: UInt)
/**
* Sets the receive buffer size of the socket
* @see java.net.SocketOptions.SO_RCVBUF
*/
@Throws(IjentUnavailableException::class)
suspend fun setReceiveBufferSize(size: UInt)
/**
* Sets the keep alive option for the socket
* @see java.net.SocketOptions.SO_KEEPALIVE
*/
@Throws(IjentUnavailableException::class)
suspend fun setKeepAlive(keepAlive: Boolean)
/**
* Sets the possibility to reuse address of the socket
* @see java.net.SocketOptions.SO_REUSEADDR
*/
@Throws(IjentUnavailableException::class)
suspend fun setReuseAddr(reuseAddr: Boolean)
/**
* Sets linger timeout for the socket
* @see java.net.SocketOptions.SO_LINGER
*/
@Throws(IjentUnavailableException::class)
suspend fun setLinger(lingerInterval: Duration)
/**
* Disables pending data until acknowledgement
* @see java.net.SocketOptions.TCP_NODELAY
*/
@Throws(IjentUnavailableException::class)
suspend fun setNoDelay(noDelay: Boolean)
/**
* Closes the connection to the socket.
*/
@Throws(IjentUnavailableException::class)
suspend fun close()
}
sealed class RemoteNetworkException(message: String) : IOException(message) {
constructor() : this("")
class ConnectionReset : RemoteNetworkException()
class ConnectionAborted : RemoteNetworkException()
class UnknownFailure(error: String) : RemoteNetworkException(error)
}
}
/**
* Convenience operator to decompose connection to a pair of channels when needed.
* @return channel to server
*/
operator fun Connection.component1(): SendChannel<ByteBuffer> = channelToServer
/**
* Convenience operator to decompose connection to a pair of channels when needed.
* @return channel from server
*/
operator fun Connection.component2(): ReceiveChannel<ByteBuffer> = channelFromServer
interface IjentTunnelsPosixApi : IjentTunnelsApi {
/**
* Creates a remote UNIX socket forwarding. IJent listens for a connection on the remote machine, and when the connection
* is accepted, the IDE communicates to the remote client via a pair of Kotlin channels.
*
* Packets sent to the channel and received from the channel may be split and/or concatenated.
* The packets may be split only if their size exceeds [com.intellij.platform.ijent.spi.RECOMMENDED_MAX_PACKET_SIZE].
*
* The call accepts only one connection. If multiple connections should be accepted, the function is supposed to be called in a loop:
* ```kotlin
* val ijent: IjentApi = ijentApiFactory()
*
* val (socketPath, tx, rx) = listenOnUnixSocket(CreateFilePath.MkTemp(prefix = "ijent-", suffix = ".sock"))
* println(socketPath) // /tmp/ijent-12345678.sock
* launch {
* handleConnection(tx, rx)
* }
* while (true) {
* val (_, tx, rx) = listenOnUnixSocket(CreateFilePath.Fixed(socketPath))
* launch {
* handleConnection(tx, rx)
* }
* }
* ```
*/
@Throws(IjentUnavailableException::class)
suspend fun listenOnUnixSocket(path: CreateFilePath = CreateFilePath.MkTemp()): ListenOnUnixSocketResult
data class ListenOnUnixSocketResult(
val unixSocketPath: String,
val tx: SendChannel<ByteBuffer>,
val rx: ReceiveChannel<ByteBuffer>,
)
sealed interface CreateFilePath {
data class Fixed(val path: String) : CreateFilePath
/** When [directory] is empty, the usual tmpdir is used. */
data class MkTemp(val directory: String = "", val prefix: String = "", val suffix: String = "") : CreateFilePath
}
}
interface IjentTunnelsWindowsApi : IjentTunnelsApi
/**
* Convenience function for working with a connection to a remote server.
*
* Example:
* ```kotlin
*
* suspend fun foo() {
* ijentTunnelsApi.withConnectionToRemotePort("localhost", 8080, {
* myErrorReporter.report(it)
* }) { (channelTo, channelFrom) ->
* handleConnection(channelTo, channelFrom)
* }
* }
*
* ```
*
* If the connection could not be established, then [errorHandler] is invoked.
* Otherwise, [action] is invoked. The connection gets automatically closed when [action] finishes.
*
* @see com.intellij.platform.ijent.IjentTunnelsApi.getConnectionToRemotePort for more details on the behavior of [Connection]
*/
@Throws(IjentUnavailableException::class)
suspend fun <T> IjentTunnelsApi.withConnectionToRemotePort(
hostAddress: IjentTunnelsApi.HostAddress,
errorHandler: suspend (IjentConnectionError) -> T,
action: suspend CoroutineScope.(Connection) -> T,
): T =
when (val connectionResult = getConnectionToRemotePort(hostAddress)) {
is IjentNetworkResult.Error -> errorHandler(connectionResult.error)
is Ok -> try {
coroutineScope { action(connectionResult.value) }
}
finally {
connectionResult.value.close()
}
}
@Throws(IjentUnavailableException::class)
suspend fun <T> IjentTunnelsApi.withConnectionToRemotePort(
host: String, port: UShort,
errorHandler: suspend (IjentConnectionError) -> T,
action: suspend CoroutineScope.(Connection) -> T,
): T = withConnectionToRemotePort(hostAddressBuilder(port).hostname(host).build(), errorHandler, action)
@Throws(IjentUnavailableException::class)
suspend fun <T> IjentTunnelsApi.withConnectionToRemotePort(
remotePort: UShort,
errorHandler: suspend (IjentConnectionError) -> T,
action: suspend CoroutineScope.(Connection) -> T,
): T = withConnectionToRemotePort("localhost", remotePort, errorHandler, action)
/**
* Represents a common class for all network-related errors appearing during the interaction with IJent
*/
sealed interface IjentNetworkError
/**
* Represents a result of a network operation
*/
sealed interface IjentNetworkResult<out T, out E : IjentNetworkError> {
/**
* Used when a network operation completed successfully
*/
interface Ok<out T> : IjentNetworkResult<T, Nothing> {
val value: T
}
/**
* Used when a network operation completed with an error
*/
interface Error<out E : IjentNetworkError> : IjentNetworkResult<Nothing, E> {
val error: E
}
}
/**
* An error that can happen during the creation of a connection to a remote server
*/
interface IjentConnectionError : IjentNetworkError {
val message: @NlsSafe String
data object ConnectionTimeout : IjentConnectionError {
override val message: @NlsSafe String = "Connection could not be established because of timeout"
}
/**
* Returned when a hostname on the remote server was resolved to multiple different addresses.
*/
data object AmbiguousAddress : IjentConnectionError {
override val message: String = "Hostname could not be resolved uniquely"
}
/**
* Returned when a socket could not be created because of an OS error.
*/
@JvmInline
value class SocketCreationFailure(override val message: @NlsSafe String) : IjentConnectionError
/**
* Returned when resolve of remote address failed during the creation of a socket.
*/
object HostUnreachable : IjentConnectionError {
override val message: @NlsSafe String = "Remote host is unreachable"
}
/**
* Returned when the remote server does not accept connections.
*/
object ConnectionRefused : IjentConnectionError {
override val message: @NlsSafe String = "Connection was refused by remote server"
}
/**
* Returned when hostname could not be resolved.
*/
@JvmInline
value class ResolveFailure(override val message: @NlsSafe String) : IjentConnectionError
/**
* Unknown failure during a connection establishment
*/
@JvmInline
value class UnknownFailure(override val message: @NlsSafe String) : IjentConnectionError
}
sealed interface IjentTunnelsApi
interface IjentTunnelsPosixApi : EelTunnelsPosixApi, IjentTunnelsApi
interface IjentTunnelsWindowsApi : EelTunnelsWindowsApi, IjentTunnelsApi

View File

@@ -14,7 +14,7 @@ import java.nio.ByteBuffer
sealed interface IjentFileSystemApi {
/**
* The same as the user from [com.intellij.platform.ijent.IjentApi.info].
* The same as the user from [com.intellij.platform.eel.IjentApi.info].
*
* There's a duplication of methods because [user] is required for checking file permissions correctly, but also it can be required
* in other cases outside the filesystem.

View File

@@ -1,7 +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.fs
import com.intellij.platform.ijent.IjentPlatform
import com.intellij.platform.eel.EelPlatform
import com.intellij.platform.ijent.fs.IjentPath.Absolute.OS
import java.nio.file.InvalidPathException
@@ -233,8 +233,8 @@ fun <P : IjentPath> IjentPathResult<P>.getOrThrow(): P =
is IjentPathResult.Err -> throw InvalidPathException(raw, reason)
}
val IjentPlatform.pathOs: OS
val EelPlatform.pathOs: OS
get() = when (this) {
is IjentPlatform.Posix -> OS.UNIX
is IjentPlatform.Windows -> OS.WINDOWS
is EelPlatform.Posix -> OS.UNIX
is EelPlatform.Windows -> OS.WINDOWS
}

View File

@@ -4,7 +4,7 @@ package com.intellij.platform.ijent.spi
import com.intellij.execution.CommandLineUtil.posixQuote
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.*
import com.intellij.platform.ijent.IjentPlatform
import com.intellij.platform.eel.EelPlatform
import com.intellij.platform.ijent.getIjentGrpcArgv
import com.intellij.util.io.computeDetached
import com.intellij.util.io.copyToAsync
@@ -48,7 +48,7 @@ abstract class IjentDeployingOverShellProcessStrategy(scope: CoroutineScope) : I
context
}
final override suspend fun getTargetPlatform(): IjentPlatform.Posix {
final override suspend fun getTargetPlatform(): EelPlatform.Posix {
return myContext.await().execCommand {
getTargetPlatform()
}
@@ -265,7 +265,7 @@ internal suspend fun createDeployingContext(runWhichCmd: suspend (commands: Coll
)
}
private suspend fun DeployingContextAndShell.getTargetPlatform(): IjentPlatform.Posix = run {
private suspend fun DeployingContextAndShell.getTargetPlatform(): EelPlatform.Posix = run {
// There are two arguments in `uname` that can show the process architecture: `-m` and `-p`. According to `man uname`, `-p` is more
// verbose, and that information may be sufficient for choosing the right binary.
// https://man.freebsd.org/cgi/man.cgi?query=uname&sektion=1
@@ -275,8 +275,8 @@ private suspend fun DeployingContextAndShell.getTargetPlatform(): IjentPlatform.
val targetPlatform = when {
arch.isEmpty() -> throw IjentStartupError.IncompatibleTarget("Empty output of `uname`")
"x86_64" in arch -> IjentPlatform.X8664Linux
"aarch64" in arch -> IjentPlatform.Aarch64Linux
"x86_64" in arch -> EelPlatform.X8664Linux
"aarch64" in arch -> EelPlatform.Aarch64Linux
else -> throw IjentStartupError.IncompatibleTarget("No binary for architecture $arch")
}
return targetPlatform

View File

@@ -1,7 +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.ijent.IjentPlatform
import com.intellij.platform.eel.EelPlatform
import com.intellij.platform.ijent.deploy
import org.jetbrains.annotations.ApiStatus
import java.nio.file.Path
@@ -13,7 +13,7 @@ import java.nio.file.Path
*
* Every instance of [IjentDeployingStrategy] is used to start exactly one IJent.
*
* @see com.intellij.platform.ijent.IjentApi
* @see com.intellij.platform.eel.IjentApi
*/
@ApiStatus.OverrideOnly
interface IjentDeployingStrategy {
@@ -25,7 +25,7 @@ interface IjentDeployingStrategy {
*
* @see com.intellij.platform.ijent.IjentExecFileProvider.getIjentBinary
*/
suspend fun getTargetPlatform(): IjentPlatform
suspend fun getTargetPlatform(): EelPlatform
/**
* Defines a set of options for connecting to a running IJent
@@ -66,11 +66,11 @@ interface IjentDeployingStrategy {
interface Posix : IjentDeployingStrategy {
/** @see [IjentDeployingStrategy.getTargetPlatform] */
override suspend fun getTargetPlatform(): IjentPlatform.Posix
override suspend fun getTargetPlatform(): EelPlatform.Posix
}
interface Windows : IjentDeployingStrategy {
/** @see [IjentDeployingStrategy.getTargetPlatform] */
override suspend fun getTargetPlatform(): IjentPlatform.Windows
override suspend fun getTargetPlatform(): EelPlatform.Windows
}
}

View File

@@ -2,11 +2,12 @@
package com.intellij.platform.ijent.spi
import com.intellij.openapi.components.serviceAsync
import com.intellij.platform.eel.EelPlatform
import com.intellij.platform.ijent.*
/**
* Given that there is some IJent process launched, this extension gets handles to stdin+stdout of the process and returns
* an [IjentApi] instance for calling procedures on IJent side.
* an [com.intellij.platform.eel.IjentApi] instance for calling procedures on IJent side.
*/
interface IjentSessionProvider {
/**
@@ -14,7 +15,7 @@ interface IjentSessionProvider {
*/
suspend fun connect(
strategy: IjentConnectionStrategy,
platform: IjentPlatform,
platform: EelPlatform,
mediator: IjentSessionMediator,
): IjentApi
@@ -39,7 +40,7 @@ sealed class IjentStartupError : RuntimeException {
}
internal class DefaultIjentSessionProvider : IjentSessionProvider {
override suspend fun connect(strategy: IjentConnectionStrategy, platform: IjentPlatform, mediator: IjentSessionMediator): IjentApi {
override suspend fun connect(strategy: IjentConnectionStrategy, platform: EelPlatform, mediator: IjentSessionMediator): IjentApi {
throw IjentStartupError.MissingImplPlugin()
}
}
@@ -51,7 +52,7 @@ internal class DefaultIjentSessionProvider : IjentSessionProvider {
* The process terminates automatically only when the IDE exits, or if [IjentApi.close] is called explicitly.
* [com.intellij.platform.ijent.bindToScope] may be useful for terminating the IJent process earlier.
*/
suspend fun connectToRunningIjent(ijentName: String, strategy: IjentConnectionStrategy, platform: IjentPlatform, process: Process): IjentApi {
suspend fun connectToRunningIjent(ijentName: String, strategy: IjentConnectionStrategy, platform: EelPlatform, process: Process): IjentApi {
val ijentSessionRegistry = IjentSessionRegistry.instanceAsync()
val ijentId = ijentSessionRegistry.register(ijentName, oneOff = true) { ijentId ->
val mediator = IjentSessionMediator.create(process, ijentId)
@@ -62,9 +63,9 @@ suspend fun connectToRunningIjent(ijentName: String, strategy: IjentConnectionSt
}
/** A specialized overload of [connectToRunningIjent] */
suspend fun connectToRunningIjent(ijentName: String, strategy: IjentConnectionStrategy, platform: IjentPlatform.Posix, process: Process): IjentPosixApi =
connectToRunningIjent(ijentName, strategy, platform as IjentPlatform, process) as IjentPosixApi
suspend fun connectToRunningIjent(ijentName: String, strategy: IjentConnectionStrategy, platform: EelPlatform.Posix, process: Process): IjentPosixApi =
connectToRunningIjent(ijentName, strategy, platform as EelPlatform, process) as IjentPosixApi
/** A specialized overload of [connectToRunningIjent] */
suspend fun connectToRunningIjent(ijentName: String, strategy: IjentConnectionStrategy, platform: IjentPlatform.Windows, process: Process): IjentWindowsApi =
connectToRunningIjent(ijentName, strategy, platform as IjentPlatform, process) as IjentWindowsApi
suspend fun connectToRunningIjent(ijentName: String, strategy: IjentConnectionStrategy, platform: EelPlatform.Windows, process: Process): IjentWindowsApi =
connectToRunningIjent(ijentName, strategy, platform as EelPlatform, process) as IjentWindowsApi

View File

@@ -4,13 +4,14 @@
package com.intellij.platform.ijent.tunnels
import com.intellij.openapi.diagnostic.Logger
import com.intellij.platform.eel.EelTunnelsApi
import com.intellij.platform.eel.withConnectionToRemotePort
import com.intellij.platform.ijent.*
import com.intellij.util.io.toByteArray
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.ClosedSendChannelException
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel
import org.jetbrains.annotations.ApiStatus
import java.net.InetSocketAddress
import java.net.ServerSocket
import java.net.Socket
@@ -20,9 +21,10 @@ import java.nio.ByteBuffer
import java.util.concurrent.CancellationException
import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit
import com.intellij.platform.eel.component1
import com.intellij.platform.eel.component2
private val LOG: Logger = Logger.getInstance(IjentTunnelsApi::class.java)
private val LOG: Logger = Logger.getInstance(EelTunnelsApi::class.java)
/**
* A tunnel to a remote server (a.k.a. local port forwarding).
@@ -36,7 +38,7 @@ private val LOG: Logger = Logger.getInstance(IjentTunnelsApi::class.java)
*
* This function returns when the server starts accepting connections.
*/
fun CoroutineScope.forwardLocalPort(tunnels: IjentTunnelsApi, localPort: Int, address: IjentTunnelsApi.HostAddress) {
fun CoroutineScope.forwardLocalPort(tunnels: EelTunnelsApi, localPort: Int, address: EelTunnelsApi.HostAddress) {
val serverSocket = ServerSocket()
serverSocket.bind(InetSocketAddress("localhost", localPort))
serverSocket.soTimeout = 2.seconds.toInt(DurationUnit.MILLISECONDS)
@@ -120,7 +122,7 @@ private fun CoroutineScope.redirectIJentDataToClientConnection(connectionId: Int
}
outputStream.flush()
}
catch (e: IjentTunnelsApi.RemoteNetworkException) {
catch (e: EelTunnelsApi.RemoteNetworkException) {
LOG.warn("Connection $connectionId closed", e)
}
}

View File

@@ -2,6 +2,7 @@
package com.intellij.execution.ijent
import com.intellij.execution.process.SelfKiller
import com.intellij.platform.eel.EelProcess
import com.intellij.platform.ijent.IjentChildProcess
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.pty4j.PtyProcess
@@ -53,7 +54,7 @@ class IjentChildPtyProcessAdapter(
try {
delegate.ijentChildProcess.resizePty(columns = winSize.columns, rows = winSize.rows)
}
catch (err: IjentChildProcess.ResizePtyError) {
catch (err: EelProcess.ResizePtyError) {
// The other implementation throw IllegalStateException in such cases.
throw IllegalStateException(err.message, err)
}

View File

@@ -1,6 +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.execution.ijent
import com.intellij.platform.eel.EelProcess
import com.intellij.platform.ijent.IjentChildProcess
import com.intellij.platform.util.coroutines.channel.ChannelOutputStream
import kotlinx.coroutines.runBlocking
@@ -32,7 +33,7 @@ internal class IjentStdinOutputStream(
ijentChildProcess.sendStdinWithConfirmation(byteArrayOf())
})
}
catch (err: IjentChildProcess.SendStdinError) {
catch (err: EelProcess.SendStdinError) {
throw IOException("Failed to flush the output stream: ${err.message}", err)
}
}

View File

@@ -12,8 +12,8 @@ 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.ijent.IjentChildProcess
import com.intellij.platform.ijent.IjentExecApi
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.util.concurrency.annotations.RequiresBlockingContext
import com.intellij.util.suspendingLazy
@@ -117,7 +117,7 @@ fun runProcessBlocking(
val exePath = FileUtil.toSystemIndependentName(args.removeFirst())
val pty = ptyOptions?.run {
IjentExecApi.Pty(initialColumns, initialRows, !consoleMode)
EelExecApi.Pty(initialColumns, initialRows, !consoleMode)
}
val workingDirectory = processBuilder.directory()?.toPath()?.let { windowsWorkingDirectory ->
@@ -136,13 +136,13 @@ fun runProcessBlocking(
.workingDirectory(workingDirectory)
.execute()
) {
is IjentExecApi.ExecuteProcessResult.Success ->
processResult.process.toProcess(
is EelExecApi.ExecuteProcessResult.Success ->
(processResult.process as IjentChildProcess).toProcess(
coroutineScope = scope,
isPty = pty != null,
redirectStderr = processBuilder.redirectErrorStream(),
)
is IjentExecApi.ExecuteProcessResult.Failure -> throw IOException(processResult.message)
is EelExecApi.ExecuteProcessResult.Failure -> throw IOException(processResult.message)
}
}

View File

@@ -99,5 +99,6 @@
<orderEntry type="module" module-name="intellij.platform.util.io.storages" />
<orderEntry type="library" name="kotlinx-collections-immutable" level="project" />
<orderEntry type="library" scope="TEST" name="ktor-server-cio" level="project" />
<orderEntry type="module" module-name="intellij.platform.eel" scope="TEST" />
</component>
</module>

View File

@@ -14,6 +14,8 @@ import com.intellij.openapi.util.Computable
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.use
import com.intellij.platform.eel.EelExecApi
import com.intellij.platform.eel.EelPlatform
import com.intellij.platform.ijent.*
import com.intellij.platform.ijent.fs.IjentFileSystemPosixApi
import com.intellij.testFramework.junit5.TestApplication
@@ -504,7 +506,7 @@ class WSLDistributionTest {
enum class WslTestStrategy { Legacy, Ijent }
private class MockIjentApi(private val adapter: GeneralCommandLine, val rootUser: Boolean) : IjentPosixApi {
override val platform: IjentPlatform get() = throw UnsupportedOperationException()
override val platform: EelPlatform get() = throw UnsupportedOperationException()
override val isRunning: Boolean get() = true
@@ -522,7 +524,7 @@ private class MockIjentApi(private val adapter: GeneralCommandLine, val rootUser
}
private class MockIjentExecApi(private val adapter: GeneralCommandLine, private val rootUser: Boolean) : IjentExecApi {
override fun executeProcessBuilder(exe: String): IjentExecApi.ExecuteProcessBuilder =
override fun executeProcessBuilder(exe: String): EelExecApi.ExecuteProcessBuilder =
MockIjentApiExecuteProcessBuilder(adapter.apply { exePath = exe }, rootUser)
override suspend fun fetchLoginShellEnvVariables(): Map<String, String> = mapOf("SHELL" to TEST_SHELL)
@@ -533,38 +535,38 @@ private val TEST_ROOT_USER_SET by lazy { Key.create<Boolean>("TEST_ROOT_USER_SET
private class MockIjentApiExecuteProcessBuilder(
private val adapter: GeneralCommandLine,
rootUser: Boolean,
) : IjentExecApi.ExecuteProcessBuilder {
) : EelExecApi.ExecuteProcessBuilder {
init {
if (rootUser) {
adapter.putUserData(TEST_ROOT_USER_SET, true)
}
}
override fun args(args: List<String>): IjentExecApi.ExecuteProcessBuilder = apply {
override fun args(args: List<String>): EelExecApi.ExecuteProcessBuilder = apply {
adapter.parametersList.run {
clearAll()
addAll(args)
}
}
override fun env(env: Map<String, String>): IjentExecApi.ExecuteProcessBuilder = apply {
override fun env(env: Map<String, String>): EelExecApi.ExecuteProcessBuilder = apply {
adapter.environment.run {
clear()
putAll(env)
}
}
override fun pty(pty: IjentExecApi.Pty?): IjentExecApi.ExecuteProcessBuilder = this
override fun pty(pty: EelExecApi.Pty?): EelExecApi.ExecuteProcessBuilder = this
override fun workingDirectory(workingDirectory: String?): IjentExecApi.ExecuteProcessBuilder = apply {
override fun workingDirectory(workingDirectory: String?): EelExecApi.ExecuteProcessBuilder = apply {
adapter.setWorkDirectory(workingDirectory)
}
override suspend fun execute(): IjentExecApi.ExecuteProcessResult = executeResultMock
override suspend fun execute(): EelExecApi.ExecuteProcessResult = executeResultMock
}
private val executeResultMock by lazy {
IjentExecApi.ExecuteProcessResult.Failure(errno = 12345, message = "mock result ${Ksuid.generate()}")
EelExecApi.ExecuteProcessResult.Failure(errno = 12345, message = "mock result ${Ksuid.generate()}")
}
private class WslTestStrategyExtension