Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,7 @@ public interface EimConstants

String USER_EIM_DIR = Paths.get(System.getProperty("user.home"), ".espressif", "eim_gui").toString(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$

String USER_EIM_CLI_DIR = Paths.get(System.getProperty("user.home"), ".espressif", "eim").toString(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$

String EIM_JSON_VALID_VERSION = "2.0"; //$NON-NLS-1$
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,18 @@ private void logError(String message)
*/
public LaunchResult launchEimWithResult(String eimPath) throws IOException
{
LaunchResult result = launchService.launch(eimPath);
return launchEimWithResult(eimPath, new String[0]);
}

/**
* Launches EIM with optional arguments (e.g. "gui") and returns the {@link com.espressif.idf.core.tools.launch.LaunchResult}.
*/
public LaunchResult launchEimWithResult(String eimPath, String... args) throws IOException
{
LaunchResult result = launchService.launch(eimPath, args);
this.lastLaunchResult = result;

logMessage("Launched EIM application: " + eimPath + " (pid=" + result.pid().orElse(-1) + ")\n"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
logMessage("Launched EIM application: " + eimPath + " " + String.join(" ", args) + " (pid=" + result.pid().orElse(-1) + ")\n"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
return result;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
Expand Down Expand Up @@ -65,9 +67,88 @@ private String findEimOnSystemPath()
}

/**
* Resolves the EIM executable path: <strong>system {@code PATH} first</strong>, then {@code eimPath} from
* {@code eim_idf.json} when the path exists on disk, then {@code EIM_PATH} env variable, then
* {@link #getDefaultEimPath()} (existence-checked).
* Probes well-known package manager bin directories for the {@code eim} executable. GUI-launched Eclipse processes
* on macOS/Linux often have a minimal PATH that excludes directories like {@code /opt/homebrew/bin}, so this method
* checks those locations directly regardless of the JVM's process PATH.
*
* @return absolute path to the executable, or empty if not found in any known location
*/
private String findEimInPackageManagerPaths()
{
String os = Platform.getOS();
String home = System.getProperty("user.home"); //$NON-NLS-1$
boolean isWindows = Platform.OS_WIN32.equals(os);
String execName = isWindows ? "eim.exe" : "eim"; //$NON-NLS-1$ //$NON-NLS-2$

List<String> candidateDirs = new ArrayList<>();

if (Platform.OS_MACOSX.equals(os))
{
candidateDirs.add("/opt/homebrew/bin"); //$NON-NLS-1$
candidateDirs.add("/usr/local/bin"); //$NON-NLS-1$
candidateDirs.add("/opt/local/bin"); //$NON-NLS-1$
}
else if (Platform.OS_LINUX.equals(os))
{
candidateDirs.add("/usr/local/bin"); //$NON-NLS-1$
candidateDirs.add("/usr/bin"); //$NON-NLS-1$
if (home != null)
{
candidateDirs.add(home + "/.local/bin"); //$NON-NLS-1$
}
candidateDirs.add("/snap/bin"); //$NON-NLS-1$
candidateDirs.add("/var/lib/flatpak/exports/bin"); //$NON-NLS-1$
if (home != null)
{
candidateDirs.add(home + "/.local/share/flatpak/exports/bin"); //$NON-NLS-1$
candidateDirs.add(home + "/.nix-profile/bin"); //$NON-NLS-1$
}
candidateDirs.add("/nix/var/nix/profiles/default/bin"); //$NON-NLS-1$
}
else if (isWindows)
{
String localAppData = System.getenv("LOCALAPPDATA"); //$NON-NLS-1$
if (localAppData != null)
{
candidateDirs.add(localAppData + "\\Microsoft\\WinGet\\Links"); //$NON-NLS-1$
}
String chocoInstall = System.getenv("ChocolateyInstall"); //$NON-NLS-1$
if (chocoInstall != null && !chocoInstall.isBlank())
{
candidateDirs.add(chocoInstall + "\\bin"); //$NON-NLS-1$
}
else
{
candidateDirs.add("C:\\ProgramData\\chocolatey\\bin"); //$NON-NLS-1$
}
if (home != null)
{
candidateDirs.add(home + "\\scoop\\shims"); //$NON-NLS-1$
}
}

for (String dir : candidateDirs)
{
Path candidate = Paths.get(dir, execName);
if (Files.isRegularFile(candidate) && Files.isExecutable(candidate))
{
return candidate.toString();
}
}

return StringUtil.EMPTY;
}

/**
* Resolves the EIM executable path using priority-based resolution:
* <ol>
* <li>System {@code PATH}</li>
* <li>Well-known package manager directories (Homebrew, MacPorts, WinGet, Chocolatey, Scoop, etc.)</li>
* <li>{@code eimPath} from {@code eim_idf.json} (when the path exists on disk)</li>
* <li>{@code EIM_PATH} env variable (existence-checked)</li>
* <li>Default GUI install location (existence-checked)</li>
* <li>Default CLI install location (existence-checked)</li>
* </ol>
*
* @param eimJson parsed JSON or {@code null}
* @return resolved absolute path string, or empty if nothing could be resolved
Expand All @@ -80,6 +161,12 @@ public String resolveEimExecutablePath(EimJson eimJson)
return fromPath;
}

String fromPkgMgr = findEimInPackageManagerPaths();
if (!StringUtil.isEmpty(fromPkgMgr))
{
return fromPkgMgr;
}

if (eimJson != null && !StringUtil.isEmpty(eimJson.getEimPath()))
{
String jsonPath = eimJson.getEimPath();
Expand All @@ -101,6 +188,12 @@ public String resolveEimExecutablePath(EimJson eimJson)
return defaultEimPath.toString();
}

Path cliEimPath = getDefaultCliEimPath();
if (cliEimPath != null && Files.exists(cliEimPath))
{
return cliEimPath.toString();
}

return StringUtil.EMPTY;
}

Expand Down Expand Up @@ -226,5 +319,58 @@ else if (os.equals(Platform.OS_MACOSX))

return defaultEimPath;
}


/**
* Returns the default CLI EIM binary path per platform. Unlike the GUI path, this points to the CLI-only install
* directory ({@code ~/.espressif/eim/}).
*/
public Path getDefaultCliEimPath()
{
String userHome = System.getProperty("user.home"); //$NON-NLS-1$
String os = Platform.getOS();
if (os.equals(Platform.OS_WIN32))
{
return Paths.get(userHome, ".espressif", "eim", "eim.exe"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
return Paths.get(userHome, ".espressif", "eim", "eim"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}

/**
* Checks whether the EIM binary at the given path supports GUI mode by running {@code eim gui --help} and checking
* for a successful exit code. This is used to determine whether to launch EIM as a GUI application or in CLI/wizard
* mode.
*
* @param eimPath absolute path to the EIM executable
* @return {@code true} if the binary supports the {@code gui} subcommand, {@code false} otherwise
*/
public boolean isEimGuiCapable(String eimPath)
{
if (StringUtil.isEmpty(eimPath))
{
return false;
}

try
{
ProcessBuilder pb = new ProcessBuilder(eimPath, "gui", "--help"); //$NON-NLS-1$ //$NON-NLS-2$
Logger.log("Checking if EIM supports GUI mode with command: " + String.join(" ", pb.command())); //$NON-NLS-1$ //$NON-NLS-2$
pb.redirectErrorStream(true);
Process process = pb.start();
// Drain stdout so the process doesn't block
process.getInputStream().transferTo(OutputStream.nullOutputStream());
boolean finished = process.waitFor(5, TimeUnit.SECONDS);
if (!finished)
{
process.destroyForcibly();
return false;
}
return process.exitValue() == 0;
}
catch (IOException | InterruptedException e)
{
Logger.log("EIM does not support the gui subcommand, falling back to CLI mode."); //$NON-NLS-1$
return false;
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,23 @@ public EimLaunchService(Display display, MessageConsoleStream standardConsoleStr
}

public LaunchResult launch(String eimPath) throws IOException
{
return launch(eimPath, new String[0]);
}

public LaunchResult launch(String eimPath, String... args) throws IOException
{
if (eimPath == null || eimPath.isBlank())
throw new IOException("EIM path is null/blank"); //$NON-NLS-1$

if (!Files.exists(Paths.get(eimPath)))
throw new IOException("EIM path not found: " + eimPath); //$NON-NLS-1$

return strategy.launch(eimPath);
if (args == null || args.length == 0)
{
return strategy.launch(eimPath);
}
return strategy.launch(eimPath, args);
}

public IStatus waitForExit(LaunchResult launchResult, IProgressMonitor monitor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,7 @@ public interface EimLauncherStrategy
{
LaunchResult launch(String eimPath) throws IOException;

LaunchResult launch(String eimPath, String... args) throws IOException;

IStatus waitForExit(LaunchResult launchResult, IProgressMonitor monitor);
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,19 @@ public LinuxEimLauncherStrategy(Display display, MessageConsoleStream standardCo

@Override
public LaunchResult launch(String eimPath) throws IOException
{
return launch(eimPath, new String[0]);
}

@Override
public LaunchResult launch(String eimPath, String... args) throws IOException
{
if (!Files.exists(Paths.get(eimPath)))
throw new IOException("EIM path not found: " + eimPath); //$NON-NLS-1$

String quotedPath = ProcessUtils.bashSingleQuote(eimPath);
String bashCmd = "nohup " + quotedPath + " > /dev/null 2>&1 & echo $!"; //$NON-NLS-1$ //$NON-NLS-2$
String argsStr = (args != null && args.length > 0) ? " " + String.join(" ", args) : ""; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
String bashCmd = "nohup " + quotedPath + argsStr + " > /dev/null 2>&1 & echo $!"; //$NON-NLS-1$ //$NON-NLS-2$
Comment on lines +47 to +48
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Quote each CLI arg before composing the bash command.

Line 47 and Line 48 concatenate raw args into bash -lc, which allows shell metacharacter injection and breaks args containing spaces/quotes.

Proposed fix
-		String argsStr = (args != null && args.length > 0) ? " " + String.join(" ", args) : ""; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+		String argsStr = (args != null && args.length > 0)
+				? " " + java.util.Arrays.stream(args).map(ProcessUtils::bashSingleQuote).collect(java.util.stream.Collectors.joining(" ")) //$NON-NLS-1$
+				: StringUtil.EMPTY;
 		String bashCmd = "nohup " + quotedPath + argsStr + " > /dev/null 2>&1 & echo $!"; //$NON-NLS-1$ //$NON-NLS-2$
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/LinuxEimLauncherStrategy.java`
around lines 47 - 48, The code in LinuxEimLauncherStrategy builds argsStr by
joining raw args which allows shell injection and breaks on spaces; change
construction of argsStr to individually shell-quote each element of args before
joining (escape any single quotes inside an arg by replacing ' with '"'"' and
wrap each arg in single quotes), then build bashCmd using quotedPath plus this
safely quoted argsStr; update references around the args, argsStr and bashCmd
variables in the class to use the per-arg quoting logic.


List<String> command = List.of("bash", "-lc", bashCmd); //$NON-NLS-1$ //$NON-NLS-2$
Process launcher = new ProcessBuilder(command).redirectErrorStream(true).start();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ public final class MacOsEimLauncherStrategy extends AbstractLoggingLauncherStrat
private static final String MACOS_LAUNCH_AND_PID_APPLESCRIPT = """
set appPath to system attribute "APP_PATH"
set bundleId to system attribute "BUNDLE_ID"
set openArgs to system attribute "OPEN_ARGS"

do shell script "open -a " & quoted form of appPath
do shell script "open -a " & quoted form of appPath & openArgs

tell application "System Events"
repeat 300 times
Expand All @@ -62,14 +63,28 @@ public MacOsEimLauncherStrategy(Display display, MessageConsoleStream standardCo
@Override
public LaunchResult launch(String eimPath) throws IOException
{
return launch(eimPath, new String[0]);
}

@Override
public LaunchResult launch(String eimPath, String... args) throws IOException
{
if (!isAppBundle(eimPath))
{
return launchCliDirect(eimPath, args);
}

String appBundlePath = deriveAppBundlePath(eimPath);
String execPath = deriveExecPath(eimPath, appBundlePath);
String bundleId = readBundleId(appBundlePath);

String argsForOpen = (args != null && args.length > 0) ? " --args " + String.join(" ", args) : ""; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$

ProcessBuilder pb = new ProcessBuilder("osascript", "-"); //$NON-NLS-1$ //$NON-NLS-2$
pb.redirectErrorStream(true);
pb.environment().put("APP_PATH", appBundlePath); //$NON-NLS-1$
pb.environment().put("BUNDLE_ID", bundleId); //$NON-NLS-1$
pb.environment().put("OPEN_ARGS", argsForOpen); //$NON-NLS-1$
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Process p = pb.start();
try (OutputStream stdin = p.getOutputStream())
Expand All @@ -94,20 +109,52 @@ public LaunchResult launch(String eimPath) throws IOException
Logger.log("BUNDLE_ID=" + bundleId); //$NON-NLS-1$
Logger.log("Launcher output was:\n" + out); //$NON-NLS-1$

// If osascript failed or returned no pid, we fall back to execPath polling.
Long pid = ProcessUtils.parseFirstLongLine(out);
if (exit == 0 && pid != null)
{
return LaunchResult.ofPid(pid.longValue(), execPath, out);
}

// Fallback (still "successfully launched" from user perspective):
// - we already attempted "open -a"
// - we can wait for closure using pgrep -f execPath
return LaunchResult.ofNoPid(execPath,
"osascript exit=" + exit + "\n" + out); //$NON-NLS-1$ //$NON-NLS-2$
}

private LaunchResult launchCliDirect(String eimPath, String... args) throws IOException
{
String quotedPath = ProcessUtils.bashSingleQuote(eimPath);
String argsStr = (args != null && args.length > 0) ? " " + String.join(" ", args) : ""; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
String bashCmd = "nohup " + quotedPath + argsStr + " > /dev/null 2>&1 & echo $!"; //$NON-NLS-1$ //$NON-NLS-2$

Process launcher = new ProcessBuilder("bash", "-lc", bashCmd) //$NON-NLS-1$ //$NON-NLS-2$
.redirectErrorStream(true).start();

String out = ProcessUtils.readAll(launcher.getInputStream());
Long pid = ProcessUtils.parseFirstLongLine(out);

if (pid == null)
{
Logger.log("macOS CLI launcher output was:\n" + out); //$NON-NLS-1$
throw new IOException("No PID found in launcher output. Output was:\n" + out); //$NON-NLS-1$
}

return LaunchResult.ofPid(pid.longValue(), eimPath, out);
}

private boolean isAppBundle(String eimPath)
{
Path p = Paths.get(eimPath).toAbsolutePath().normalize();
while (p != null)
{
String name = p.getFileName() != null ? p.getFileName().toString() : ""; //$NON-NLS-1$
if (name.endsWith(".app")) //$NON-NLS-1$
{
return true;
}
p = p.getParent();
}
return false;
}

@Override
public IStatus waitForExit(LaunchResult launchResult, IProgressMonitor monitor)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,21 @@ public WindowsEimLauncherStrategy(Display display, MessageConsoleStream standard

@Override
public LaunchResult launch(String eimPath) throws IOException
{
return launch(eimPath, new String[0]);
}

@Override
public LaunchResult launch(String eimPath, String... args) throws IOException
{
if (!Files.exists(Paths.get(eimPath)))
throw new IOException("EIM path not found: " + eimPath); //$NON-NLS-1$

String escapedPathForPowershell = eimPath.replace("'", "''"); //$NON-NLS-1$ //$NON-NLS-2$
String argsStr = (args != null && args.length > 0) ? " -ArgumentList '" + String.join(" ", args) + "'" : ""; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
String powershellCmd = String.format(
"Start-Process -FilePath '%s' -PassThru | Select-Object -ExpandProperty Id", //$NON-NLS-1$
escapedPathForPowershell);
"Start-Process -FilePath '%s'%s -PassThru | Select-Object -ExpandProperty Id", //$NON-NLS-1$
escapedPathForPowershell, argsStr);
Comment on lines +47 to +50
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Escape and structure PowerShell -ArgumentList safely.

Line 47–Line 50 inject raw joined args into a quoted PowerShell expression. Args containing ' (or complex quoting) can break command parsing and may enable command injection.

Proposed fix
-		String argsStr = (args != null && args.length > 0) ? " -ArgumentList '" + String.join(" ", args) + "'" : ""; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
+		String argsStr = StringUtil.EMPTY;
+		if (args != null && args.length > 0)
+		{
+			String quotedArgs = java.util.Arrays.stream(args)
+					.map(a -> "'" + a.replace("'", "''") + "'") //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+					.collect(java.util.stream.Collectors.joining(", ")); //$NON-NLS-1$
+			argsStr = " -ArgumentList " + quotedArgs; //$NON-NLS-1$
+		}
 		String powershellCmd = String.format(
 				"Start-Process -FilePath '%s'%s -PassThru | Select-Object -ExpandProperty Id", //$NON-NLS-1$
 				escapedPathForPowershell, argsStr);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/WindowsEimLauncherStrategy.java`
around lines 47 - 50, The code builds powershellCmd by injecting raw joined args
into a single-quoted -ArgumentList string which can break parsing or allow
injection when args contain single quotes or other special characters; update
WindowsEimLauncherStrategy to build argsStr by escaping each argument separately
(replace any single quote ' with two single quotes ''), wrap each escaped
argument in single quotes, and pass them as a comma-separated list to
-ArgumentList (e.g. " -ArgumentList " + String.join(", ", escapedArgs)); use
escapedPathForPowershell for -FilePath as before and ensure the args null/empty
case still yields an empty argsStr.


List<String> command = List.of("powershell.exe", "-Command", powershellCmd); //$NON-NLS-1$ //$NON-NLS-2$
Process launcher = new ProcessBuilder(command).redirectErrorStream(true).start();
Expand Down
Loading
Loading