The JetBrains Blog

The Dev Containers Story: Introducing EelApi for Plugin Authors

8.5内容质量

TL;DR · AI 摘要

JetBrains 推出 EelApi,解决 Dev Containers 和 WSL 环境中插件开发的路径和进程管理问题。

核心要点

  • EelApi 为插件作者提供了在 Dev Containers 和 WSL 环境中处理路径和进程的 API。
  • WSL2 和容器环境导致传统 IDE 模型不再适用,需要新的解决方案。
  • JetBrains 通过一个名为 ijent 的代理,为 IDE 提供了与目标环境的通信通道。

结构提纲

按章节快速跳转。

  1. 现代开发环境的变化对插件开发提出了新的挑战。

  2. 传统 IDE 模型无法适应 WSL 和 Dev Containers 环境,需要新的解决方案。

  3. EelApi 的引入

    EelApi 为插件作者提供了在 Dev Containers 和 WSL 环境中处理路径和进程的 API。

  4. JetBrains 使用一个名为 ijent 的代理,为 IDE 提供了与目标环境的通信通道。

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • EelApi 的引入
    • 背景与挑战
      • WSL2 和容器环境的隔离问题
      • 传统 IDE 模型的局限性
    • 解决方案
      • EelApi 的功能
      • ijent 代理的使用

金句 / Highlights

值得收藏与分享的关键句。

  • WSL2 带来了 Linux 项目环境,但与 Windows 进程隔离,导致路径和进程管理变得复杂。

    第 3 段

    ⬇︎ 下载 PNG𝕏 分享到 X
  • Dev Containers 推动了更进一步的隔离,使得在容器中运行完整 IDE 后端可能过于复杂。

    第 4 段

    ⬇︎ 下载 PNG𝕏 分享到 X
  • JetBrains 通过一个名为 ijent 的代理,为 IDE 提供了与目标环境的通信通道。

    第 5 段

    ⬇︎ 下载 PNG𝕏 分享到 X
#JetBrains#Dev Containers#WSL#插件开发#EelApi
打开原文

Dev 容器的故事:为插件作者引入 EelApi - JetBrains 博客

JetBrains 平台

JetBrains 产品的插件和扩展开发。

关注

  • 关注:
  • X X
  • RSS RSS

前往 Marketplace

IntelliJ 平台

插件

Dev 容器的故事:为插件作者引入 EelApi

Alexander Koshevoy

现代开发显著改变了旧的 IDE 模式之一:现在,项目不仅可能不托管在与 IDE 实例相同的物理或远程机器上,甚至可能两者共享同一台主机,但彼此在隔离的环境中分离。如果你是插件作者,这种实际影响可能非常具体。你的插件下载一个 CLI 工具,启动它,将项目路径传递给它,从它那里接收环境变量,或向它启动的某个东西打开 TCP 连接。它在本地运行。然后用户在 WSL 或 Dev 容器中打开同一个项目,突然之间,“本地路径”、“当前操作系统”、“localhost”和“启动一个进程”这些词都需要更多的精确性。

首先,一点背景。当清楚 EelApi 存在的原因时,它的使用会变得容易得多。如果你已经在为 WSL 或 Dev 容器使插件工作,并且想要 API 的详细信息,请跳到本指南的“从项目开始”部分。

为什么这个 API 存在

最初的 IDE 模型非常简单。IDE、项目文件、SDK、工具、环境变量和进程都位于用户的机器上。一个路径指向 IDE 可以读取的文件。SystemInfo 描述了运行工具的操作系统。ProcessBuilder 在正确的位置启动进程,因为只有一个“位置”。

然后项目变得足够大,开发环境变得足够专业,使得适合项目的正确机器有时是完全不同的机器:办公室的工作站、云中的虚拟机,或由团队准备的主机。在这种情况下,自然的解决方案是将 IDE 的智能部分靠近项目:在文件、SDK 和工具附近运行完整的 IDE 后端,并从轻量级前端连接到它。

WSL2 和容器创造了另一种问题。

WSL2 将 Linux 项目环境带到了与 IDE 相同的物理机器上,但将其与 IDE 运行的 Windows 进程隔离开。项目文件可以存在于 Linux 文件系统中,工具必须作为 Linux 进程运行,路径字符串必须在 Linux 一侧有意义。

Dev 容器进一步推动了这种模式。容器足够隔离,拥有自己的文件系统、二进制文件、环境变量、进程和网络命名空间。同时,它与主机 IDE 足够接近,以至于在其中启动完整的 IDE 后端可能比任务所需的机器更多。

这就是我们开始探索的空白:IDE 可以留在本地,而项目侧的操作可以在项目实际生活的环境中进行吗?

对这一挑战的第一次回应不是公开的 API。它是一个代理。

在目标环境中,一个小型的 IntelliJ 平台代理(ijent)为 IDE 提供了一条受控的通道,可以访问文件系统、进程、端口和平台信息。我们尝试了多种传输方式和 RPC 形式,围绕需求逐渐形成了一套以 Kotlin 为主的内部 API:挂起操作、结构化的生命周期、项目感知的作用域、数据流式传输,以及足够的文件系统语义,以处理真实的 WSL 和容器项目。

该 API 被命名为 EelApi。

EelApi 是 IntelliJ 平台用于与执行环境交互的 API:本地机器、WSL 发行版、Docker 容器或 Dev Container。

NIO Path 可以被教着去访问另一个环境。有时候这已经足够。但进程执行、操作系统检测、目标端的路径字符串、环境变量、网络以及优化的文件系统操作需要一个相同缺失的概念:一个显式的执行环境。

WSL 作为第一个生产案例

WSL 是底层技术的第一个大规模验证案例。

IDE 保留在 Windows 上,而项目则运行在 Linux 环境中。这意味着 Linux 文件系统语义、符号链接、Linux SDK 和 Linux 端的工具。同时,IntelliJ IDEA 已经有多年 WSL 特定集成的基础可供使用。

基于代理的文件系统通道让我们能够从平台内部改进这一模型。在 IntelliJ IDEA 2024.3 中,WSL 项目支持增加了符号链接支持,并将 IDE 与 WSL 的通信切换为 Hyper-V 套接字。在 IntelliJ IDEA 2025.1 中,WSL 项目的索引速度比 Windows 项目更快,完全支持符号链接,并且在 WSL 中使用 JDK 更加无缝。

此时,公开的 EelApi 还未完成。但其背后的通道已经能够执行实际任务,这比一张漂亮的图表是更好的测试。

从 WSL 到 Dev Container

Dev Container 带来了不同的挑战。

对于 WSL,平台已经有了长期的 WSL 特定集成历史:路径识别、通过 WSL 机制启动进程,以及在不同子系统中的专用桥梁。基于代理的文件系统通道可以改进并替换这些现有功能的一部分。

Dev Container 并没有同样成熟的 IntelliJ 特定基础。将容器化项目视为本地项目意味着从一开始就围绕代理构建环境访问模型,这意味着文件系统访问、进程执行、平台信息、路径转换和网络访问都必须通过相同的通道。

在 IntelliJ IDEA 2025.3 中,这表现为在同一个 IDE 窗口中打开 Dev Container 项目的一个选项。在 IntelliJ IDEA 2026.1 中,这成为 Dev Container 的默认工作流程:项目在本地 IDE 中打开,而无需在容器内启动完整的 IDE 后端。

这就是我们在这篇文章中所说的原生 Dev Container 支持:本地 IDE、容器端的 IntelliJ 平台代理,以及通过执行环境 API 路由的项目环境操作。

在 IntelliJ IDEA 中,这现在涵盖了 Dev Container 项目预期的主要工作流程。剩余的工作是将这种支持扩展到更多的 IDE、更多的语言栈、更多的平台子系统以及更多的第三方插件场景。

关于 API 状态的说明

在进入代码示例之前,这里有一个你应该注意的实用细节:目前 EelApi 的大部分接口都标记为 @ApiStatus.Experimental。

对于插件作者来说,这并不意味着“请暂时忽略这个问题”。这意味着当你开发的插件针对 WSL 或 Dev Containers 时,应朝着这个方向进行尝试,同时在稳定过程中仍有可能进行一些源代码级别的调整。我们预计这些更改将受到限制,因为相同模型已经在生产环境中用于 WSL 支持和基于 IntelliJ 的 IDE 的原生 Dev Container 支持。

从项目开始

大多数插件代码应从 Project 开始。

如果在 IDE 中打开了一个项目,平台已经建立了与该项目交互所需的环境。对于本地项目,该环境是本地计算机。对于 WSL 项目,该环境是 WSL 发行版。对于以原生模式打开的 Dev Container 项目,该环境是容器。

code
import com.intellij.platform.eel.provider.getEelDescriptor
import com.intellij.platform.eel.provider.toEelApi

val descriptor = project.getEelDescriptor()
val eel = descriptor.toEelApi()

你也可以从 Path 开始:

code
import com.intellij.platform.eel.provider.getEelDescriptor

val descriptor = path.getEelDescriptor()

这在你处理的路径本身是你要操作的对象时非常有用。不过,对于任意路径,要更加小心。将路径视为环境不仅仅是字符串解析;当你实际使用描述符时,平台可能需要启动、部署或连接到一个 IntelliJ 平台代理,某些环境可能不可用。一个已经打开的项目是最安全的锚点,因为如果没有访问工作环境的权限,该项目将不会以该模式打开。

描述符和机器

EelDescriptor 回答:“通过哪条路径可以访问这个环境?”

当你需要在环境中执行操作或在 Path 和 EelPath 之间进行转换时,使用描述符。

EelMachine 回答:“这实际上是哪台底层机器、容器或发行版?”

多个描述符可能指向同一台机器。例如,通过 \\wsl$ 和 \\wsl.localhost 的 WSL 路径可以指向同一个 WSL 发行版。当你管理共享资源(如连接池、长期运行的服务、按环境状态或可重复使用的隧道)时,使用 EelMachine 作为缓存键。

大多数插件代码应从 EelDescriptor 开始。当你有意在多个访问路径之间共享某些内容时,应使用 EelMachine。

code
import com.intellij.platform.eel.fs.createTemporaryDirectory
import com.intellij.platform.eel.getOrThrow
import com.intellij.platform.eel.provider.asNioPath
import com.intellij.platform.eel.provider.getEelDescriptor
import com.intellij.platform.eel.provider.toEelApi
import java.nio.file.Files
import java.nio.file.StandardCopyOption

val eel = project.getEelDescriptor().toEelApi()

val remoteDir = eel.fs.createTemporaryDirectory()
  .prefix("my-plugin-")
  .deleteOnExit(true)
  .getOrThrow()
  .asNioPath()

val remoteBinary = Files.copy(
  localBinary,
  remoteDir.resolve(localBinary.fileName),
  StandardCopyOption.REPLACE_EXISTING,
)

在此处,remoteDir 仍然是一个 java.nio.file.Path,但它指向项目环境。Files.copyFiles.write 和其他 NIO 操作会通过环境感知的文件系统提供程序进行路由。

还有一个内部工具,可以一步将本地内容传输到远程环境。随着 EelApi 的稳定,计划提供一个对应的公共工具。

在环境中运行工具

当进程属于项目环境时,请使用 EelApi.exec,而不是 ProcessBuilder

code
import com.intellij.platform.eel.provider.asEelPath
import com.intellij.platform.eel.provider.getEelDescriptor
import com.intellij.platform.eel.provider.toEelApi
import com.intellij.platform.eel.provider.utils.readAllBytes
import com.intellij.platform.eel.spawnProcess

val eel = project.getEelDescriptor().toEelApi()

val process = eel.exec.spawnProcess(remoteBinary.asEelPath().toString())
  .workingDirectory(projectRoot.asEelPath())
  .args("--version")
  .eelIt()

val exitCode = process.exitCode.await()
val stdout = process.stdout.readAllBytes().toString(Charsets.UTF_8)

使用 EelPath 作为工作目录。如果参数是一个目标进程将读取的路径,请传递目标端的路径字符串。

传递环境变量和目标端路径

构建工具是一个很好的例子,因为它们既作为参数传递路径,也作为环境变量传递路径。假设一个插件需要在项目环境中使用特定的 JDK 和自定义设置文件启动 Maven。

code
import com.intellij.platform.eel.provider.asEelPath

val javaHomeInTarget = jdkHome.asEelPath().toString()

val settingsXmlInTarget = mavenSettingsXml.asEelPath().toString()

val process = eel.exec.spawnProcess(mavenExecutable.asEelPath().toString())
  .workingDirectory(projectRoot.asEelPath())
  .env(mapOf("JAVA_HOME" to javaHomeInTarget))
  .args("-s", settingsXmlInTarget, "test")
  .eelIt()

对于 Linux 容器,JAVA_HOME 应该如下所示:

code
/usr/lib/jvm/java-21-openjdk

而不是像主机端的路由路径那样。同样的规则适用于 Go 特定的路径,如 GOROOTGOPATHGOMODCACHE,以及更专业的变量,如 LD_PRELOAD:如果环境中的进程将读取变量值作为路径,请提供该环境中看到的路径。

检测目标平台

不要使用 SystemInfo 来选择项目环境的二进制文件。SystemInfo 描述的是 IDE 主机。使用 eel.platform 来确定项目端工具将运行的环境。

code
val classifier = when {
  eel.platform.isWindows -> "windows-x64"
  eel.platform.isMac -> "macos-aarch64"
  eel.platform.isPosix -> "linux-x64"
  else -> error("Unsupported environment")
}

当 IDE 主机是 macOS 或 Windows,但项目在 Linux 容器中运行时,这一点尤为重要。

跨边界连接端口

如果 IDE 侧的代码需要与环境内部监听的服务进行通信,请不要假设 localhost 在两侧具有相同含义。

对于一次性连接,通过 EelTunnelsApi 进行连接。在此示例中,localhost 在项目环境中被解析:

code
import com.intellij.platform.eel.getConnectionToRemotePort
import com.intellij.platform.eel.withConnectionToRemotePort
import java.io.IOException

eel.tunnels.getConnectionToRemotePort()
  .hostname("localhost")
  .port(servicePort.toUShort())
  .withConnectionToRemotePort(
    errorHandler = { error -> throw IOException("Cannot connect to service in the project environment", error) },
  ) { connection ->
    connection.sendChannel.send(requestBytes)
    val response = connection.receiveChannel.receive(8192)
    handleResponse(response)
  }

对于一个现有的 IDE 侧库,它只知道如何连接到本地 TCP 端口,可以创建一个代理。该代理监听在 IDE 主机上,并将流量转发到环境中的服务:

code
import com.intellij.platform.eel.eelProxy
import com.intellij.platform.eel.provider.localEel
import com.intellij.platform.eel.provider.utils.acceptOnTcpPort
import com.intellij.platform.eel.provider.utils.connectToTcpPort
import kotlinx.coroutines.launch

val proxy = eelProxy()
  .acceptOnTcpPort(localEel.tunnels, port = 0u)
  .connectToTcpPort(eel.tunnels, host = "localhost", port = servicePort.toUShort())
  .eelIt()
val localPort = proxy.acceptor.boundAddress.port.toInt()
val proxyJob = scope.launch {
  proxy.runForever()
}

现在,IDE 侧的代码可以连接到 127.0.0.1:$localPort。保持代理的生命周期明确:当服务、运行配置、调试会话或工具窗口不再需要隧道时,取消该任务。

还有一种方向是接受环境内部的连接,并将其转发回 IDE 主机。当容器中的进程需要回调 IDE 侧服务时,这非常有用。

理解路径

上面的示例同时使用了 Path 和目标侧路径字符串。这是最容易出错的部分,因此值得单独处理。

一个 java.nio.file.Path 是 IDE/JVM 侧的路径。这是 IntelliJ 平台 API 和标准 Java 文件 API 可以接受的形式。

在 Windows 上使用 WSL 时,这个路径是熟悉的:

code
\\wsl.localhost\Ubuntu\home\user\project
\\wsl$\Ubuntu\home\user\project

目标侧的路径是 WSL 内部 Linux 工具所看到的路径:

code
/home/user/project

这些形式自然地映射:UNC 路径标识了 WSL 发行版和其中的 Linux 路径。具备 EEL 意识的文件访问可以识别 WSL 路径,将其与 WSL 环境关联,并通过代理支持的通道路由文件操作。

Docker 和 Dev Containers 需要一个合成的路由路径,因为主机操作系统没有通往运行中容器内文件的正常本地路径。

在 Windows 上,Dev Container 路由路径可能在概念上看起来像这样:

code
//devcontainer.ij/devcontainer-abc@np~.~pipe~docker_engine/workspaces/app

或者以 Windows 风格显示:

code
\\devcontainer.ij\devcontainer-abc@np~.~pipe~docker_engine\workspaces\app

在 Linux 或 macOS 上,同样的想法使用 Unix 风格的合成根:

code
/$devcontainer.ij/devcontainer-abc@/workspaces/app
/$devcontainer.ij/devcontainer-abc@u~var~run~docker.sock/workspaces/app

前缀标识了一个 Dev Container 的路由路径。内路径之前的部分标识了容器和 Docker 端点。内路径是容器内部的路径:

code
/workspaces/app

路由路径对 IntelliJ 平台和 JetBrains 运行时文件 API 有重要意义。它不是一个普通的主机文件系统路径,不应期望任意的主机进程能够理解。

因此,规则如下:

  • 对于 IntelliJ 平台 API 和 Java 文件操作,使用 Path。
  • 对于传递给在环境中运行的进程的路径,使用 EelPath 或 path.asEelPath().toString()。
  • 不要将合成的 Docker 路由路径传递给不相关的主机工具。

现有的以本地为中心的 API 会怎样?

IntelliJ 平台有许多最初是为本地机器模型设计的 API。其中一些已经了解了足够的路由路径信息,可以在 WSL 和 Dev Container 项目中继续正常工作。

NIO Path 是最重要的一个例子:如果 Path 属于 WSL 或 Dev Container,标准操作如 Files.exists、Files.copy 或 Files.newInputStream 可以路由到环境感知的文件系统提供者。

在运行于 JetBrains 运行时的 IDE 中,还存在一个兼容层,用于较旧的 java.io.File 代码。JBR 可以通过对应的 NIO 实现将 File 操作路由,因此从路由路径创建的 File 可以到达相同的环境感知文件系统提供者,并且对于 WSL 或 Dev Container,还可以到达底层的 IntelliJ Agent。请将此视为对现有代码的兼容性;对于新代码,建议使用 Path,因为它具有现代的、提供者感知的 Java 文件系统 API,并且可以直接与 Eel API(如 asEelPath() 和 asNioPath())组合使用。

GeneralCommandLine 也可以参与这一模型。在支持的情况下,可执行文件路径或工作目录可以让平台选择正确的环境来执行进程。

这一点非常重要,因为这些 API 并不会将每个字符串重新解释为路径。命令行参数、环境变量、配置文件和协议消息中也可能包含路径。如果目标进程将读取该值,请使用 asEelPath() 显式将其转换为目标端的形式。

对于主机信息也是如此。SystemInfo 和 System.getenv() 描述的是主机上的 IDE 进程。它们并不是对 WSL 分发版或项目端进程将运行的容器的描述。

这与远程开发和 Split 模式的关系

远程开发使用了独立的 IDE 进程——一个轻量级的前端和一个靠近项目的完整后端。Split 模式是该世界中的插件架构:插件代码可能需要前端、后端和共享部分。

EelApi 回答的是另一个问题:此操作在何处运行?

如果你的插件需要代码在不同的 IDE 进程中运行,你仍然需要远程开发插件模型。如果你的插件需要运行 CLI、检查目标操作系统信息、转换路径、访问文件或在 WSL 或 Dev Container 中打开端口,EelApi 是你应该查看的环境 API。

对于在原生模式下的 IntelliJ IDEA Dev Container,Split 模式不再只是为了让项目环境操作在容器中运行所必需的。在原生模式不可用的情况下,远程开发仍然是打开和使用 Dev Container 项目的选项。

未来计划

我们计划稳定公共 API 的接口,发布专门的文档,并为插件作者提供迁移指导。

在基于 IntelliJ 的 IDE 中,这一趋势继续延续:更广泛的 Dev Container 覆盖范围,更多的子系统采用,以及插件作者需要判断项目是本地、WSL 还是容器化的场景更少。

来自真实插件的反馈现在尤其有用,因为公共接口仍处于实验阶段,还有调整的空间。

EelApi

插件开发

  • 分享
  • Facebook
  • Twitter
  • LinkedIn

上一篇

使用可选内容模块构建 IntelliJ 插件

在 IntelliJ IDEA 2026.2 中开源 LSP 客户端 API

下一篇