diff --git a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimConstants.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimConstants.java index c60353014..413561415 100644 --- a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimConstants.java +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimConstants.java @@ -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$ } diff --git a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/ToolInitializer.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/ToolInitializer.java index 3a9b90844..4ad906f51 100644 --- a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/ToolInitializer.java +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/ToolInitializer.java @@ -11,6 +11,7 @@ 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; @@ -65,9 +66,14 @@ private String findEimOnSystemPath() } /** - * Resolves the EIM executable path: system {@code PATH} first, 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). + * Resolves the EIM executable path using priority-based resolution: + *
    + *
  1. System {@code PATH}
  2. + *
  3. {@code eimPath} from {@code eim_idf.json} (when the path exists on disk)
  4. + *
  5. {@code EIM_PATH} env variable (existence-checked)
  6. + *
  7. Default GUI install location (existence-checked)
  8. + *
  9. Default CLI install location (existence-checked)
  10. + *
* * @param eimJson parsed JSON or {@code null} * @return resolved absolute path string, or empty if nothing could be resolved @@ -101,6 +107,12 @@ public String resolveEimExecutablePath(EimJson eimJson) return defaultEimPath.toString(); } + Path cliEimPath = getDefaultCliEimPath(); + if (cliEimPath != null && Files.exists(cliEimPath)) + { + return cliEimPath.toString(); + } + return StringUtil.EMPTY; } @@ -227,5 +239,55 @@ 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$ + pb.redirectErrorStream(true); + Process process = pb.start(); + 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; + } + } + } diff --git a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/MacOsEimLauncherStrategy.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/MacOsEimLauncherStrategy.java index 9632e34b0..72dc7f4b0 100644 --- a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/MacOsEimLauncherStrategy.java +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/MacOsEimLauncherStrategy.java @@ -62,6 +62,11 @@ public MacOsEimLauncherStrategy(Display display, MessageConsoleStream standardCo @Override public LaunchResult launch(String eimPath) throws IOException { + if (!isAppBundle(eimPath)) + { + return launchCliDirect(eimPath); + } + String appBundlePath = deriveAppBundlePath(eimPath); String execPath = deriveExecPath(eimPath, appBundlePath); String bundleId = readBundleId(appBundlePath); @@ -108,6 +113,41 @@ public LaunchResult launch(String eimPath) throws IOException "osascript exit=" + exit + "\n" + out); //$NON-NLS-1$ //$NON-NLS-2$ } + private LaunchResult launchCliDirect(String eimPath) throws IOException + { + String quotedPath = ProcessUtils.bashSingleQuote(eimPath); + String bashCmd = "nohup " + quotedPath + " > /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) {