[terminal] IJPL-163095 launch local/WSL shell with EelApi

Enable `terminal.use.EelApi` registry key for that (by default, it's disabled).

GitOrigin-RevId: 1ff0343d8919b0ddf13642626210a2e40f93d72c
This commit is contained in:
Sergey Simonchik
2025-03-19 04:19:13 +00:00
committed by intellij-monorepo-bot
parent 457e48fca6
commit 20dd88bceb
5 changed files with 135 additions and 13 deletions
+2
View File
@@ -111,5 +111,7 @@
<orderEntry type="library" scope="TEST" name="assertJ" level="project" />
<orderEntry type="module" module-name="intellij.platform.kernel" />
<orderEntry type="module" module-name="intellij.platform.project" />
<orderEntry type="module" module-name="intellij.platform.eel" />
<orderEntry type="module" module-name="intellij.platform.eel.provider" />
</component>
</module>
@@ -63,6 +63,9 @@
<registryKey key="terminal.classic.refresh.vfs.on.shell.command.finished" defaultValue="true"
description="Refresh VFS when shell commands are finished in the Classic Terminal.
May not work 100% reliably as it uses heuristics to determine when command is finished."/>
<registryKey key="terminal.use.EelApi" defaultValue="false"
description="Use EelApi to launch a shell process. Launching a local shell works the same as before.
For WSL environments, the shell process in WSL will be launched via IJEnt instead of using wsl.exe."/>
<experimentalFeature id="terminal.shell.command.handling" percentOfUsers="100"/>
<terminal.shellCommandHandler implementation="org.jetbrains.plugins.terminal.RunAnythingTerminalBridge"/>
@@ -36,6 +36,7 @@ import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.jetbrains.plugins.terminal.LocalBlockTerminalRunner.*;
import static org.jetbrains.plugins.terminal.TerminalStartupKt.startProcess;
import static org.jetbrains.plugins.terminal.util.ShellNameUtil.*;
public class LocalTerminalDirectRunner extends AbstractTerminalRunner<PtyProcess> {
@@ -110,21 +111,33 @@ public class LocalTerminalDirectRunner extends AbstractTerminalRunner<PtyProcess
TerminalUsageLocalStorage.getInstance().recordBlockTerminalUsed();
}
Path workingDirPath = null;
try {
workingDirPath = Path.of(workingDir);
}
catch (InvalidPathException ignored) {
}
try {
long startNano = System.nanoTime();
PtyProcess process = (PtyProcess)ProcessService.getInstance().startPtyProcess(
command,
workingDir,
envs,
LocalPtyOptions.defaults().builder()
.initialColumns(initialTermSize != null ? initialTermSize.getColumns() : -1)
.initialRows(initialTermSize != null ? initialTermSize.getRows() : -1)
.build(),
null,
false,
false,
false
);
PtyProcess process;
if (workingDirPath != null && shouldUseEelApi()) {
process = startProcess(List.of(command), envs, workingDirPath, Objects.requireNonNull(initialTermSize));
}
else {
process = (PtyProcess)ProcessService.getInstance().startPtyProcess(
command,
workingDir,
envs,
LocalPtyOptions.defaults().builder()
.initialColumns(initialTermSize != null ? initialTermSize.getColumns() : -1)
.initialRows(initialTermSize != null ? initialTermSize.getRows() : -1)
.build(),
null,
false,
false,
false
);
}
LOG.info("Started " + process.getClass().getName() + " in " + TimeoutUtil.getDurationMillis(startNano) + " ms from "
+ stringifyProcessInfo(command, workingDir, initialTermSize, envs, !LOG.isDebugEnabled()));
return process;
@@ -266,4 +279,7 @@ public class LocalTerminalDirectRunner extends AbstractTerminalRunner<PtyProcess
|| shellName.equals(FISH_NAME) && Registry.is(BLOCK_TERMINAL_FISH_REGISTRY, false);
}
private static boolean shouldUseEelApi() {
return Registry.is("terminal.use.EelApi", false);
}
}
@@ -4,6 +4,7 @@ package org.jetbrains.plugins.terminal.arrangement;
import com.google.common.util.concurrent.Futures;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.execution.ijent.IjentChildPtyProcessAdapter;
import com.intellij.execution.process.CapturingProcessRunner;
import com.intellij.execution.process.OSProcessHandler;
import com.intellij.execution.process.ProcessOutput;
@@ -38,6 +39,8 @@ public final class ProcessInfoUtil {
}
private static @Nullable String doGetCwd(@NotNull Process process) throws Exception {
// use shell integration instead
if (process instanceof IjentChildPtyProcessAdapter) return null;
if (SystemInfo.isUnix) {
int pid = (int)process.pid();
String result = tryGetCwdFastOnUnix(pid);
@@ -1,9 +1,24 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.terminal
import com.intellij.execution.wsl.WSLDistribution
import com.intellij.execution.wsl.WslPath
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.progress.runBlockingMaybeCancellable
import com.intellij.openapi.util.SystemInfo
import com.intellij.platform.eel.EelApi
import com.intellij.platform.eel.EelExecApi
import com.intellij.platform.eel.EelExecApi.ExecuteProcessError
import com.intellij.platform.eel.EelResult
import com.intellij.platform.eel.provider.asEelPath
import com.intellij.platform.eel.provider.getEelDescriptor
import com.intellij.util.PathUtil
import com.jediterm.core.util.TermSize
import com.jediterm.terminal.TtyConnector
import com.pty4j.PtyProcess
import org.jetbrains.plugins.terminal.runner.LocalTerminalStartCommandBuilder
import java.nio.file.Path
import java.time.Duration
import java.time.Instant
@@ -32,4 +47,87 @@ internal fun logCommonStartupInfo(
", time to process created: ${durationBetweenStartupAndConnectorCreated.toMillis()} ms")
}
@Throws(ErrnoException::class)
internal fun startProcess(
command: List<String>,
envs: Map<String, String>,
initialWorkingDirectory: Path,
initialTermSize: TermSize,
): PtyProcess {
return runBlockingMaybeCancellable {
val (eelApi, workingDirectory) = getEelApi(initialWorkingDirectory, command)
val remoteCommand = convertCommandToRemote(eelApi, command)
doStartProcess(eelApi, remoteCommand, envs, workingDirectory, initialTermSize)
}
}
private suspend fun convertCommandToRemote(eelApi: EelApi, command: List<String>): List<String> {
if (isWslCommand(command)) {
val shell = eelApi.exec.fetchLoginShellEnvVariables()["SHELL"] ?: "/bin/sh"
return listOf(shell, LocalTerminalDirectRunner.LOGIN_CLI_OPTION, LocalTerminalStartCommandBuilder.INTERACTIVE_CLI_OPTION)
}
return command
}
private fun isWslCommand(command: List<String>): Boolean {
if (SystemInfo.isWindows) {
val exePath = command.getOrNull(0) ?: return false
val exeFileName = PathUtil.getFileName(exePath)
return exeFileName.equals("wsl.exe", true) || exeFileName.equals("wsl", true)
}
return false
}
private suspend fun getEelApi(
workingDirectory: Path,
command: List<String>,
): Pair<EelApi, Path> {
val wslDistribNameFromCommandline = getWslDistributionNameFromCommand(command)
if (wslDistribNameFromCommandline != null) {
val wslDistribNameFromWorkingDirectory = WslPath.parseWindowsUncPath(workingDirectory.toString())?.distributionId
if (wslDistribNameFromCommandline != wslDistribNameFromWorkingDirectory) {
val wslRootPath = WSLDistribution(wslDistribNameFromCommandline).getUNCRootPath()
val eelApi = wslRootPath.getEelDescriptor().upgrade()
val userHome = runCatching { eelApi.exec.fetchLoginShellEnvVariables()["HOME"] }.getOrNull()
return eelApi to wslRootPath.resolve(userHome ?: ".")
}
}
return workingDirectory.getEelDescriptor().upgrade() to workingDirectory
}
private fun getWslDistributionNameFromCommand(command: List<String>): String? {
if (isWslCommand(command)) {
val distributionOptionName = command.getOrNull(1)
if (distributionOptionName == "-d" || distributionOptionName == "--distribution") {
return command.getOrNull(2)
}
}
return null
}
@Throws(ErrnoException::class)
private suspend fun doStartProcess(
eelApi: EelApi,
command: List<String>,
envs: Map<String, String>,
workingDirectory: Path,
initialTermSize: TermSize,
): PtyProcess {
// TODO migrate to generated builders in 252 (not available in 251)
@Suppress("DEPRECATION")
val execOptions = EelExecApi.ExecuteProcessOptions.Builder(command.first())
.args(command.takeLast(command.size - 1))
.env(envs)
.workingDirectory(workingDirectory.asEelPath())
.ptyOrStdErrSettings(EelExecApi.Pty(initialTermSize.columns, initialTermSize.rows, true))
.build()
val processResult = eelApi.exec.execute(execOptions)
return when (processResult) {
is EelResult.Ok -> processResult.value.convertToJavaProcess() as PtyProcess
is EelResult.Error -> throw ErrnoException(processResult.error)
}
}
internal class ErrnoException(val error: ExecuteProcessError): Exception(error.message)
private val log: Logger = logger<AbstractTerminalRunner<*>>()