From 124f6abec805740f2a2af6079d379bc9e4de4874 Mon Sep 17 00:00:00 2001 From: Ali Azam Rana <85216275+alirana01@users.noreply.github.com> Date: Thu, 22 Jan 2026 14:20:02 +0100 Subject: [PATCH 1/3] IEP-1686: Fix for eim download getting stuck --- .../espressif/idf/core/tools/EimLoader.java | 232 ++++++++++++++---- .../idf/ui/tools/EimButtonLaunchListener.java | 10 +- .../idf/ui/tools/EspressifToolStartup.java | 6 +- 3 files changed, 191 insertions(+), 57 deletions(-) diff --git a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimLoader.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimLoader.java index 4db1005c2..9fba15aaf 100644 --- a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimLoader.java +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimLoader.java @@ -51,6 +51,39 @@ public class EimLoader { private static final String URL_JSON = "https://dl.espressif.com/dl/eim/eim_unified_release.json"; //$NON-NLS-1$ private static final Path DOWNLOAD_DIR = Paths.get(System.getProperty("java.io.tmpdir"), "eim_gui"); //$NON-NLS-1$ //$NON-NLS-2$ + private static final String MACOS_LAUNCH_AND_PID_APPLESCRIPT = """ + set appPath to system attribute "APP_PATH" + set bundlePrefix to system attribute "BUNDLE_PREFIX" + + -- Launch app + do shell script "open -a " & quoted form of appPath + + -- Try System Events first (may require Automation permission) + try + tell application "System Events" + repeat 100 times + set matches to (processes whose bundle identifier starts with bundlePrefix) + if (count of matches) > 0 then + return unix id of (item 1 of matches) + end if + delay 0.1 + end repeat + end tell + on error errMsg number errNum + -- fall through to pgrep fallback + end try + + -- Fallback: pgrep (does not require System Events) + repeat 100 times + try + set pidStr to do shell script "pgrep -fn " & quoted form of bundlePrefix + if pidStr is not "" then return pidStr as number + end try + delay 0.1 + end repeat + + error "PID not found (app may not have launched)" + """; //$NON-NLS-1$ private String os; private String arch; @@ -58,7 +91,7 @@ public class EimLoader private MessageConsoleStream standardConsoleStream; private MessageConsoleStream errorConsoleStream; private Display display; - private long windowsPid; + private long eimPid; public EimLoader(DownloadListener listener, MessageConsoleStream standardConsoleStream, MessageConsoleStream errorConsoleStream, Display display) { @@ -103,76 +136,152 @@ private void logError(String message) Logger.log(message); } - public Process launchEim(String eimPath) throws IOException + public long launchEim(String eimPath) throws IOException { if (!Files.exists(Paths.get(eimPath))) throw new FileNotFoundException("EIM path not found: " + eimPath); //$NON-NLS-1$ - String os = Platform.getOS(); - List command; + String osLocal = Platform.getOS(); - if (os.equals(Platform.OS_WIN32)) + if (osLocal.equals(Platform.OS_MACOSX)) { - String escapedPathForPowershell = eimPath.replace("'", "''"); //$NON-NLS-1$ //$NON-NLS-2$ - String powershellCmd = String.format( - "Start-Process -FilePath '%s' -PassThru | " //$NON-NLS-1$ - + "Select-Object -ExpandProperty Id", //$NON-NLS-1$ - escapedPathForPowershell); - - command = List.of("powershell.exe", //$NON-NLS-1$ - "-Command", powershellCmd); //$NON-NLS-1$ + eimPid = launchMacAndGetPid(eimPath); + logMessage("Launched EIM application: " + eimPath + " (pid=" + eimPid + ")\n"); //$NON-NLS-1$ //$NON-NLS-2$ + return eimPid; } - else if (os.equals(Platform.OS_MACOSX)) + + List command; + if (osLocal.equals(Platform.OS_WIN32)) { - command = List.of("open", "-W", "-a", eimPath); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$ + command = windowsLaunchCommand(eimPath); } - else if (os.equals(Platform.OS_LINUX)) + else if (osLocal.equals(Platform.OS_LINUX)) { - command = List.of("bash", "-c", "\"" + eimPath + "\""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ + command = linuxLaunchCommand(eimPath); } else { - throw new UnsupportedOperationException("Unsupported OS: " + os); //$NON-NLS-1$ + throw new UnsupportedOperationException("Unsupported OS: " + osLocal); //$NON-NLS-1$ } - - Process process = new ProcessBuilder(command).redirectErrorStream(true).start(); - if (os.equals(Platform.OS_WIN32)) + + Process launcher = new ProcessBuilder(command).redirectErrorStream(true).start(); + storePid(launcher); + + logMessage("Launched EIM application: " + eimPath + " (pid=" + eimPid + ")\n"); //$NON-NLS-1$ //$NON-NLS-2$ + return eimPid; + } + + private long launchMacAndGetPid(String eimPath) throws IOException + { + String bundlePrefix = "com.espressif.eim"; //$NON-NLS-1$ + String appPath = deriveAppBundlePath(eimPath); + + ProcessBuilder pb = new ProcessBuilder("osascript", "-"); //$NON-NLS-1$ //$NON-NLS-2$ + pb.redirectErrorStream(true); + + pb.environment().put("APP_PATH", appPath); //$NON-NLS-1$ + pb.environment().put("BUNDLE_PREFIX", bundlePrefix); //$NON-NLS-1$ + + Process p = pb.start(); + + // Send AppleScript via stdin + try (OutputStream stdin = p.getOutputStream()) { - // store the PID returned by powershell query to a variable - storePid(process); + stdin.write(MACOS_LAUNCH_AND_PID_APPLESCRIPT.getBytes(java.nio.charset.StandardCharsets.UTF_8)); } - logMessage("Launched EIM application: " + eimPath + "\n"); //$NON-NLS-1$ //$NON-NLS-2$ + // Read PID from stdout + long pid = readPidFromProcessOutput(p); + + Logger.log("APP_PATH=" + appPath); //$NON-NLS-1$ + // Ensure osascript finished successfully (otherwise you might have read some partial output) + try + { + int exit = p.waitFor(); + if (exit != 0) + { + throw new IOException("osascript failed (exit " + exit + ")"); //$NON-NLS-1$ //$NON-NLS-2$ + } + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while waiting for osascript", e); //$NON-NLS-1$ + } - return process; + return pid; } - - private void storePid(Process powershellProcess) + + private long readPidFromProcessOutput(Process p) throws IOException { - try (BufferedReader reader = new BufferedReader(new InputStreamReader(powershellProcess.getInputStream()))) + StringBuilder out = new StringBuilder(); + try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream(), + java.nio.charset.StandardCharsets.UTF_8))) { String line; - while ((line = reader.readLine()) != null) + while ((line = br.readLine()) != null) { - line = line.trim(); - if (!line.isEmpty()) - { - try - { - windowsPid = Long.parseLong(line); - - } - catch (NumberFormatException ignored) - { - // skipping invalid lines - } - } + out.append(line).append('\n'); + String trimmed = line.trim(); + if (trimmed.matches("\\d+")) //$NON-NLS-1$ + return Long.parseLong(trimmed); } } - catch (IOException e) + + String output = out.toString().trim(); + Logger.log("Launcher output was:\n" + output); //$NON-NLS-1$ + + throw new IOException("No PID found in launcher output. Output was:\n" + output); //$NON-NLS-1$ + } + + + private String deriveAppBundlePath(String eimPath) + { + Path p = Paths.get(eimPath).toAbsolutePath().normalize(); + + // Walk up until we find *.app + while (p != null) { - Logger.log(e); + String name = p.getFileName() != null ? p.getFileName().toString() : ""; //$NON-NLS-1$ + if (name.endsWith(".app")) //$NON-NLS-1$ + { + return p.toString(); // ALWAYS absolute due to toAbsolutePath() above + } + p = p.getParent(); } + + throw new IllegalArgumentException("Cannot derive .app bundle path from: " + eimPath); //$NON-NLS-1$ + } + + + private List windowsLaunchCommand(String eimPath) + { + String escapedPathForPowershell = eimPath.replace("'", "''"); //$NON-NLS-1$ //$NON-NLS-2$ + String powershellCmd = String.format( + "Start-Process -FilePath '%s' -PassThru | " //$NON-NLS-1$ + + "Select-Object -ExpandProperty Id", //$NON-NLS-1$ + escapedPathForPowershell); + + return List.of("powershell.exe", //$NON-NLS-1$ + "-Command", powershellCmd); //$NON-NLS-1$ + } + + private List linuxLaunchCommand(String eimPath) + { + String quotedPath = bashSingleQuote(eimPath); + String bashCmd = "nohup " + quotedPath + " > /dev/null 2>&1 & echo $!"; //$NON-NLS-1$ //$NON-NLS-2$ + return List.of("bash", "-lc", bashCmd); //$NON-NLS-1$ //$NON-NLS-2$ + } + + private String bashSingleQuote(String input) + { + return "'" + input.replace("'", "'\"'\"'") + "'"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ + } + + private void storePid(Process launcherProcess) throws IOException + { + // For Windows/Linux launchers we expect PID to be printed to stdout. + eimPid = readPidFromProcessOutput(launcherProcess); } public String installAndLaunchDmg(Path dmgPath) throws IOException, InterruptedException @@ -304,7 +413,7 @@ else if (name.endsWith(".exe")) //$NON-NLS-1$ private IStatus waitForProcessWindows() { - while (isWindowsProcessAlive(windowsPid)) + while (isWindowsProcessAlive(eimPid)) { try { @@ -333,7 +442,7 @@ private boolean isWindowsProcessAlive(long pid) String line; while ((line = reader.readLine()) != null) { - if (line.contains(String.valueOf(windowsPid))) + if (line.contains(String.valueOf(eimPid))) { return true; } @@ -348,6 +457,31 @@ private boolean isWindowsProcessAlive(long pid) return false; } + private IStatus waitForProcessByPid(long pid) + { + if (pid <= 0) + return Status.error("Invalid PID: " + pid); //$NON-NLS-1$ + + try + { + while (ProcessHandle.of(pid).map(ProcessHandle::isAlive).orElse(false)) + { + Thread.sleep(1000); + } + return Status.OK_STATUS; + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + return Status.CANCEL_STATUS; + } + catch (Exception e) + { + Logger.log(e); + return Status.error(e.getMessage()); + } + } + private IStatus waitForProcess(Process process) { try @@ -366,14 +500,14 @@ private IStatus waitForProcess(Process process) } } - public void waitForEimClosure(Process process, Runnable callback) - { + public void waitForEimClosure(long pidToWait, Runnable callback) + { Job waitJob = new Job("Wait for EIM Closure") //$NON-NLS-1$ { @Override protected IStatus run(IProgressMonitor monitor) { - return os.equals(Platform.OS_WIN32) ? waitForProcessWindows() : waitForProcess(process); + return waitForProcessByPid(pidToWait); } }; waitJob.setSystem(true); diff --git a/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EimButtonLaunchListener.java b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EimButtonLaunchListener.java index eb2eafef5..dd265eed8 100644 --- a/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EimButtonLaunchListener.java +++ b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EimButtonLaunchListener.java @@ -87,8 +87,8 @@ protected IStatus run(IProgressMonitor monitor) { try { - Process process = eimLoader.launchEim(idfEnvironmentVariables.getEnvValue(IDFEnvironmentVariables.EIM_PATH)); - eimLoader.waitForEimClosure(process, EimButtonLaunchListener.this::refreshAfterEimClose); + long eimPid = eimLoader.launchEim(idfEnvironmentVariables.getEnvValue(IDFEnvironmentVariables.EIM_PATH)); + eimLoader.waitForEimClosure(eimPid, EimButtonLaunchListener.this::refreshAfterEimClose); } catch (IOException e) { @@ -194,12 +194,12 @@ public void onCompleted(String filePath) } } - Process process; + long pidEim = -1; try { idfEnvironmentVariables.addEnvVariable(IDFEnvironmentVariables.EIM_PATH, appToLaunch); - process = eimLoader.launchEim(appToLaunch); - eimLoader.waitForEimClosure(process, EimButtonLaunchListener.this::refreshAfterEimClose); + pidEim = eimLoader.launchEim(appToLaunch); + eimLoader.waitForEimClosure(pidEim, EimButtonLaunchListener.this::refreshAfterEimClose); } catch (IOException e) { diff --git a/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EspressifToolStartup.java b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EspressifToolStartup.java index 5f53a295a..eb5687015 100644 --- a/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EspressifToolStartup.java +++ b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EspressifToolStartup.java @@ -376,7 +376,7 @@ public void onCompleted(String filePath) } }); - Process process = null; + long eimPid = -1; String appToLaunch = filePath; try { @@ -386,7 +386,7 @@ public void onCompleted(String filePath) } idfEnvironmentVariables.addEnvVariable(IDFEnvironmentVariables.EIM_PATH, appToLaunch); - process = eimLoader.launchEim(appToLaunch); + eimPid = eimLoader.launchEim(appToLaunch); } catch ( IOException @@ -395,7 +395,7 @@ public void onCompleted(String filePath) Logger.log(e); } - eimLoader.waitForEimClosure(process, () -> { + eimLoader.waitForEimClosure(eimPid, () -> { if (toolInitializer.isOldEspIdfConfigPresent() && !toolInitializer.isOldConfigExported()) { Logger.log("Old configuration found and not converted"); From fa9ce39a77e0100496f8a228ea9c537508a0f781 Mon Sep 17 00:00:00 2001 From: Ali Azam Rana <85216275+alirana01@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:16:11 +0100 Subject: [PATCH 2/3] cleaned and better design for eim launch --- .../META-INF/MANIFEST.MF | 1 + .../espressif/idf/core/tools/EimLoader.java | 529 +++++------------- .../core/tools/launch/EimLaunchService.java | 48 ++ .../core/tools/launch/EimLauncherFactory.java | 47 ++ .../idf/core/tools/launch/LaunchResult.java | 59 ++ .../idf/core/tools/launch/ProcessUtils.java | 59 ++ .../idf/core/tools/launch/ProcessWaiter.java | 104 ++++ .../AbstractLoggingLauncherStrategy.java | 65 +++ .../strategies/EimLauncherStrategy.java | 24 + .../strategies/LinuxEimLauncherStrategy.java | 63 +++ .../strategies/MacOsEimLauncherStrategy.java | 179 ++++++ .../WindowsEimLauncherStrategy.java | 65 +++ .../idf/ui/tools/EimButtonLaunchListener.java | 11 +- .../idf/ui/tools/EspressifToolStartup.java | 8 +- 14 files changed, 863 insertions(+), 399 deletions(-) create mode 100644 bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/EimLaunchService.java create mode 100644 bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/EimLauncherFactory.java create mode 100644 bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/LaunchResult.java create mode 100644 bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/ProcessUtils.java create mode 100644 bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/ProcessWaiter.java create mode 100644 bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/AbstractLoggingLauncherStrategy.java create mode 100644 bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/EimLauncherStrategy.java create mode 100644 bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/LinuxEimLauncherStrategy.java create mode 100644 bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/MacOsEimLauncherStrategy.java create mode 100644 bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/WindowsEimLauncherStrategy.java diff --git a/bundles/com.espressif.idf.core/META-INF/MANIFEST.MF b/bundles/com.espressif.idf.core/META-INF/MANIFEST.MF index ab53426c8..7cceb1532 100644 --- a/bundles/com.espressif.idf.core/META-INF/MANIFEST.MF +++ b/bundles/com.espressif.idf.core/META-INF/MANIFEST.MF @@ -38,6 +38,7 @@ Export-Package: com.espressif.idf.core, com.espressif.idf.core.toolchain, com.espressif.idf.core.tools, com.espressif.idf.core.tools.exceptions, + com.espressif.idf.core.tools.launch, com.espressif.idf.core.tools.util, com.espressif.idf.core.tools.vo, com.espressif.idf.core.tools.watcher, diff --git a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimLoader.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimLoader.java index 9fba15aaf..88ea9a67e 100644 --- a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimLoader.java +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimLoader.java @@ -13,20 +13,19 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; -import java.util.List; import java.util.Optional; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Platform; -import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.IJobChangeEvent; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.core.runtime.jobs.JobChangeAdapter; @@ -35,6 +34,8 @@ import com.espressif.idf.core.IDFEnvironmentVariables; import com.espressif.idf.core.logging.Logger; +import com.espressif.idf.core.tools.launch.EimLaunchService; +import com.espressif.idf.core.tools.launch.LaunchResult; import com.espressif.idf.core.tools.watcher.EimJsonWatchService; import com.espressif.idf.core.util.StringUtil; import com.google.gson.JsonArray; @@ -42,8 +43,9 @@ import com.google.gson.JsonParser; /** - * This class is responsible for downloading and launching the EIM. - * The clients using this must take care of UI refreshes and pausing any listeners. + * This class is responsible for downloading and launching the EIM. The clients using this must take care of UI + * refreshes and pausing any listeners. + * * @author Ali Azam Rana * */ @@ -51,61 +53,34 @@ public class EimLoader { private static final String URL_JSON = "https://dl.espressif.com/dl/eim/eim_unified_release.json"; //$NON-NLS-1$ private static final Path DOWNLOAD_DIR = Paths.get(System.getProperty("java.io.tmpdir"), "eim_gui"); //$NON-NLS-1$ //$NON-NLS-2$ - private static final String MACOS_LAUNCH_AND_PID_APPLESCRIPT = """ - set appPath to system attribute "APP_PATH" - set bundlePrefix to system attribute "BUNDLE_PREFIX" - -- Launch app - do shell script "open -a " & quoted form of appPath + private final String os; + private final String arch; + private final DownloadListener listener; + private final MessageConsoleStream standardConsoleStream; + private final MessageConsoleStream errorConsoleStream; + private final Display display; - -- Try System Events first (may require Automation permission) - try - tell application "System Events" - repeat 100 times - set matches to (processes whose bundle identifier starts with bundlePrefix) - if (count of matches) > 0 then - return unix id of (item 1 of matches) - end if - delay 0.1 - end repeat - end tell - on error errMsg number errNum - -- fall through to pgrep fallback - end try - - -- Fallback: pgrep (does not require System Events) - repeat 100 times - try - set pidStr to do shell script "pgrep -fn " & quoted form of bundlePrefix - if pidStr is not "" then return pidStr as number - end try - delay 0.1 - end repeat - - error "PID not found (app may not have launched)" - """; //$NON-NLS-1$ - - private String os; - private String arch; - private DownloadListener listener; - private MessageConsoleStream standardConsoleStream; - private MessageConsoleStream errorConsoleStream; - private Display display; - private long eimPid; - - public EimLoader(DownloadListener listener, MessageConsoleStream standardConsoleStream, MessageConsoleStream errorConsoleStream, Display display) + private final EimLaunchService launchService; + + private volatile LaunchResult lastLaunchResult; + + public EimLoader(DownloadListener listener, MessageConsoleStream standardConsoleStream, + MessageConsoleStream errorConsoleStream, Display display) { - os = Platform.getOS(); - arch = Platform.getOSArch(); + this.os = org.eclipse.core.runtime.Platform.getOS(); + this.arch = org.eclipse.core.runtime.Platform.getOSArch(); this.listener = listener; this.standardConsoleStream = standardConsoleStream; this.errorConsoleStream = errorConsoleStream; this.display = display; + + this.launchService = new EimLaunchService(display, standardConsoleStream, errorConsoleStream); } - + private void logMessage(String message) { - display.asyncExec(()->{ + display.asyncExec(() -> { try { standardConsoleStream.write(message); @@ -116,13 +91,13 @@ private void logMessage(String message) logError(e.getMessage()); } }); - + Logger.log(message); } - + private void logError(String message) { - display.asyncExec(()->{ + display.asyncExec(() -> { try { errorConsoleStream.write(message); @@ -132,156 +107,66 @@ private void logError(String message) Logger.log(e); } }); - + Logger.log(message); } - - public long launchEim(String eimPath) throws IOException - { - if (!Files.exists(Paths.get(eimPath))) - throw new FileNotFoundException("EIM path not found: " + eimPath); //$NON-NLS-1$ - - String osLocal = Platform.getOS(); - - if (osLocal.equals(Platform.OS_MACOSX)) - { - eimPid = launchMacAndGetPid(eimPath); - logMessage("Launched EIM application: " + eimPath + " (pid=" + eimPid + ")\n"); //$NON-NLS-1$ //$NON-NLS-2$ - return eimPid; - } - List command; - if (osLocal.equals(Platform.OS_WIN32)) - { - command = windowsLaunchCommand(eimPath); - } - else if (osLocal.equals(Platform.OS_LINUX)) - { - command = linuxLaunchCommand(eimPath); - } - else - { - throw new UnsupportedOperationException("Unsupported OS: " + osLocal); //$NON-NLS-1$ - } - - Process launcher = new ProcessBuilder(command).redirectErrorStream(true).start(); - storePid(launcher); - - logMessage("Launched EIM application: " + eimPath + " (pid=" + eimPid + ")\n"); //$NON-NLS-1$ //$NON-NLS-2$ - return eimPid; - } - - private long launchMacAndGetPid(String eimPath) throws IOException + /** + * Launches the eim and return {@link com.espressif.idf.core.tools.launch.LaunchResult}. + */ + public LaunchResult launchEimWithResult(String eimPath) throws IOException { - String bundlePrefix = "com.espressif.eim"; //$NON-NLS-1$ - String appPath = deriveAppBundlePath(eimPath); - - ProcessBuilder pb = new ProcessBuilder("osascript", "-"); //$NON-NLS-1$ //$NON-NLS-2$ - pb.redirectErrorStream(true); - - pb.environment().put("APP_PATH", appPath); //$NON-NLS-1$ - pb.environment().put("BUNDLE_PREFIX", bundlePrefix); //$NON-NLS-1$ - - Process p = pb.start(); - - // Send AppleScript via stdin - try (OutputStream stdin = p.getOutputStream()) - { - stdin.write(MACOS_LAUNCH_AND_PID_APPLESCRIPT.getBytes(java.nio.charset.StandardCharsets.UTF_8)); - } + LaunchResult result = launchService.launch(eimPath); + this.lastLaunchResult = result; - // Read PID from stdout - long pid = readPidFromProcessOutput(p); + logMessage("Launched EIM application: " + eimPath + " (pid=" + result.pid().orElse(-1) + ")\n"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + return result; + } - Logger.log("APP_PATH=" + appPath); //$NON-NLS-1$ - // Ensure osascript finished successfully (otherwise you might have read some partial output) - try + public void waitForEimClosure(LaunchResult launchResult, Runnable callback) + { + Job waitJob = new Job("Wait for EIM Closure") //$NON-NLS-1$ { - int exit = p.waitFor(); - if (exit != 0) + @Override + protected IStatus run(IProgressMonitor monitor) { - throw new IOException("osascript failed (exit " + exit + ")"); //$NON-NLS-1$ //$NON-NLS-2$ + return launchService.waitForExit(launchResult, monitor); } - } - catch (InterruptedException e) - { - Thread.currentThread().interrupt(); - throw new IOException("Interrupted while waiting for osascript", e); //$NON-NLS-1$ - } - - return pid; - } + }; + waitJob.setSystem(true); - private long readPidFromProcessOutput(Process p) throws IOException - { - StringBuilder out = new StringBuilder(); - try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream(), - java.nio.charset.StandardCharsets.UTF_8))) + waitJob.addJobChangeListener(new JobChangeAdapter() { - String line; - while ((line = br.readLine()) != null) + @Override + public void aboutToRun(IJobChangeEvent event) { - out.append(line).append('\n'); - String trimmed = line.trim(); - if (trimmed.matches("\\d+")) //$NON-NLS-1$ - return Long.parseLong(trimmed); + EimJsonWatchService.getInstance().pauseListeners(); } - } - - String output = out.toString().trim(); - Logger.log("Launcher output was:\n" + output); //$NON-NLS-1$ - throw new IOException("No PID found in launcher output. Output was:\n" + output); //$NON-NLS-1$ - } + @Override + public void done(IJobChangeEvent event) + { + Display.getDefault().asyncExec(() -> { + try + { + standardConsoleStream.write("EIM has been closed.\n"); //$NON-NLS-1$ + } + catch (IOException e) + { + Logger.log(e); + } + }); - - private String deriveAppBundlePath(String eimPath) - { - Path p = Paths.get(eimPath).toAbsolutePath().normalize(); + if (callback != null) + { + callback.run(); + } - // Walk up until we find *.app - while (p != null) - { - String name = p.getFileName() != null ? p.getFileName().toString() : ""; //$NON-NLS-1$ - if (name.endsWith(".app")) //$NON-NLS-1$ - { - return p.toString(); // ALWAYS absolute due to toAbsolutePath() above + EimJsonWatchService.getInstance().unpauseListeners(); } - p = p.getParent(); - } - - throw new IllegalArgumentException("Cannot derive .app bundle path from: " + eimPath); //$NON-NLS-1$ - } + }); - - private List windowsLaunchCommand(String eimPath) - { - String escapedPathForPowershell = eimPath.replace("'", "''"); //$NON-NLS-1$ //$NON-NLS-2$ - String powershellCmd = String.format( - "Start-Process -FilePath '%s' -PassThru | " //$NON-NLS-1$ - + "Select-Object -ExpandProperty Id", //$NON-NLS-1$ - escapedPathForPowershell); - - return List.of("powershell.exe", //$NON-NLS-1$ - "-Command", powershellCmd); //$NON-NLS-1$ - } - - private List linuxLaunchCommand(String eimPath) - { - String quotedPath = bashSingleQuote(eimPath); - String bashCmd = "nohup " + quotedPath + " > /dev/null 2>&1 & echo $!"; //$NON-NLS-1$ //$NON-NLS-2$ - return List.of("bash", "-lc", bashCmd); //$NON-NLS-1$ //$NON-NLS-2$ - } - - private String bashSingleQuote(String input) - { - return "'" + input.replace("'", "'\"'\"'") + "'"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ - } - - private void storePid(Process launcherProcess) throws IOException - { - // For Windows/Linux launchers we expect PID to be printed to stdout. - eimPid = readPidFromProcessOutput(launcherProcess); + waitJob.schedule(); } public String installAndLaunchDmg(Path dmgPath) throws IOException, InterruptedException @@ -328,35 +213,6 @@ public String installAndLaunchDmg(Path dmgPath) throws IOException, InterruptedE new IDFEnvironmentVariables().addEnvVariable(IDFEnvironmentVariables.EIM_PATH, eimPath); return eimPath; } - - private String readProcessOutput(Process p) throws IOException - { - try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()))) - { - StringBuilder sb = new StringBuilder(); - String line; - while ((line = br.readLine()) != null) sb.append(line).append(System.lineSeparator()); - return sb.toString(); - } - } - - private String parseVolumePath(InputStream mountOut) throws IOException - { - try (BufferedReader reader = new BufferedReader(new InputStreamReader(mountOut))) - { - String line; - while ((line = reader.readLine()) != null) - { - if (line.contains("/Volumes/")) //$NON-NLS-1$ - { - for (String part : line.split("\t")) //$NON-NLS-1$ - if (part.startsWith("/Volumes/")) return part.trim(); //$NON-NLS-1$ - } - } - } - return null; - } - public void downloadEim(IProgressMonitor monitor) { @@ -382,9 +238,10 @@ public void downloadEim(IProgressMonitor monitor) Path downloadPath = DOWNLOAD_DIR.resolve(name); downloadFile(downloadUrl, downloadPath, listener, monitor); + Path eimPath = Paths.get(EimConstants.USER_EIM_DIR); Files.createDirectories(eimPath); - + if (name.endsWith(".zip")) //$NON-NLS-1$ { Path extracted = unzip(downloadPath, eimPath); @@ -392,164 +249,28 @@ public void downloadEim(IProgressMonitor monitor) } else if (name.endsWith(".exe")) //$NON-NLS-1$ { - eimPath = Paths.get(eimPath.toString(), name); - Files.copy(downloadPath, eimPath, StandardCopyOption.REPLACE_EXISTING); - listener.onCompleted(eimPath.toString()); + Path exePath = Paths.get(eimPath.toString(), name); + Files.copy(downloadPath, exePath, StandardCopyOption.REPLACE_EXISTING); + listener.onCompleted(exePath.toString()); } - else + else { listener.onCompleted(downloadPath.toAbsolutePath().toString()); } } - catch (IOException e) + catch (IOException | URISyntaxException e) { listener.onError("Download failed", e); //$NON-NLS-1$ - } finally - { - monitor.done(); } - - } - - private IStatus waitForProcessWindows() - { - while (isWindowsProcessAlive(eimPid)) + finally { - try - { - Thread.sleep(1000); - } - catch (InterruptedException e) - { - Logger.log(e); - } - } - - return Status.OK_STATUS; - } - - private boolean isWindowsProcessAlive(long pid) - { - try - { - Process check = new ProcessBuilder("cmd.exe", //$NON-NLS-1$ - "/c", //$NON-NLS-1$ - "tasklist", //$NON-NLS-1$ - "/FI", //$NON-NLS-1$ - "\"PID eq " + pid + "\"").redirectErrorStream(true).start(); //$NON-NLS-1$ //$NON-NLS-2$ - try (BufferedReader reader = new BufferedReader(new InputStreamReader(check.getInputStream()))) - { - String line; - while ((line = reader.readLine()) != null) - { - if (line.contains(String.valueOf(eimPid))) - { - return true; - } - } - } - } - catch (IOException e) - { - Logger.log(e); + monitor.done(); } - - return false; } - - private IStatus waitForProcessByPid(long pid) - { - if (pid <= 0) - return Status.error("Invalid PID: " + pid); //$NON-NLS-1$ - try - { - while (ProcessHandle.of(pid).map(ProcessHandle::isAlive).orElse(false)) - { - Thread.sleep(1000); - } - return Status.OK_STATUS; - } - catch (InterruptedException e) - { - Thread.currentThread().interrupt(); - return Status.CANCEL_STATUS; - } - catch (Exception e) - { - Logger.log(e); - return Status.error(e.getMessage()); - } - } - - private IStatus waitForProcess(Process process) + private JsonObject fetchJson() throws IOException, URISyntaxException { - try - { - process.waitFor(); - return Status.OK_STATUS; - } - catch (InterruptedException e) - { - return Status.CANCEL_STATUS; - } - catch (Exception e) - { - Logger.log(e); - return Status.error(e.getMessage()); - } - } - - public void waitForEimClosure(long pidToWait, Runnable callback) - { - Job waitJob = new Job("Wait for EIM Closure") //$NON-NLS-1$ - { - @Override - protected IStatus run(IProgressMonitor monitor) - { - return waitForProcessByPid(pidToWait); - } - }; - waitJob.setSystem(true); - - - waitJob.addJobChangeListener(new JobChangeAdapter() - { - @Override - public void aboutToRun(IJobChangeEvent event) - { - EimJsonWatchService.getInstance().pauseListeners(); - } - - @Override - public void done(IJobChangeEvent event) - { - Display.getDefault().asyncExec(() -> { - try - { - standardConsoleStream.write("EIM has been closed.\n"); //$NON-NLS-1$ - } - catch (IOException e) - { - Logger.log(e); - } - }); - - if (callback != null) - { - callback.run(); - } - - EimJsonWatchService.getInstance().unpauseListeners(); - } - }); - - waitJob.schedule(); - } - - private JsonObject fetchJson() throws IOException - { - URL url = new URL(URL_JSON); + URL url = new URI(URL_JSON).toURL(); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestProperty("accept", "application/json"); //$NON-NLS-1$//$NON-NLS-2$ connection.setConnectTimeout(10000); @@ -561,43 +282,20 @@ private JsonObject fetchJson() throws IOException } } - private void cleanupDownloadDirectory() - { - try - { - Files.list(DOWNLOAD_DIR) - .filter(Files::isRegularFile) - .forEach(path -> { - try - { - Files.deleteIfExists(path); - } - catch (IOException e) - { - Logger.log("Failed to delete old download: " + path); //$NON-NLS-1$ - } - }); - } - catch (IOException e) - { - Logger.log("Failed to clean up download directory: " + e.getMessage()); //$NON-NLS-1$ - } - } - private Optional findMatchingAsset(JsonArray assets) { String osToken = switch (os) { - case Platform.OS_WIN32 -> "windows"; //$NON-NLS-1$ - case Platform.OS_MACOSX -> "macos"; //$NON-NLS-1$ - case Platform.OS_LINUX -> "linux"; //$NON-NLS-1$ + case org.eclipse.core.runtime.Platform.OS_WIN32 -> "windows"; //$NON-NLS-1$ + case org.eclipse.core.runtime.Platform.OS_MACOSX -> "macos"; //$NON-NLS-1$ + case org.eclipse.core.runtime.Platform.OS_LINUX -> "linux"; //$NON-NLS-1$ default -> StringUtil.EMPTY; }; String archToken = switch (arch) { - case Platform.ARCH_X86_64 -> "x64"; //$NON-NLS-1$ - case Platform.ARCH_AARCH64, "arm64" -> "aarch64"; //$NON-NLS-1$ //$NON-NLS-2$ + case org.eclipse.core.runtime.Platform.ARCH_X86_64 -> "x64"; //$NON-NLS-1$ + case org.eclipse.core.runtime.Platform.ARCH_AARCH64, "arm64" -> "aarch64"; //$NON-NLS-1$ //$NON-NLS-2$ default -> StringUtil.EMPTY; }; @@ -617,9 +315,9 @@ private Optional findMatchingAsset(JsonArray assets) } private void downloadFile(String fileURL, Path targetPath, DownloadListener listener, IProgressMonitor monitor) - throws IOException + throws IOException, URISyntaxException { - URL url = new URL(fileURL); + URL url = new URI(fileURL).toURL(); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout(10000); connection.setReadTimeout(10000); @@ -629,7 +327,6 @@ private void downloadFile(String fileURL, Path targetPath, DownloadListener list try (InputStream in = connection.getInputStream(); OutputStream out = Files.newOutputStream(targetPath)) { - byte[] buffer = new byte[8192]; int bytesRead; long totalRead = 0; @@ -653,6 +350,27 @@ private void downloadFile(String fileURL, Path targetPath, DownloadListener list } } + private void cleanupDownloadDirectory() + { + try + { + Files.list(DOWNLOAD_DIR).filter(Files::isRegularFile).forEach(path -> { + try + { + Files.deleteIfExists(path); + } + catch (IOException e) + { + Logger.log("Failed to delete old download: " + path); //$NON-NLS-1$ + } + }); + } + catch (IOException e) + { + Logger.log("Failed to clean up download directory: " + e.getMessage()); //$NON-NLS-1$ + } + } + private Path unzip(Path zipPath, Path destDir) throws IOException { Files.createDirectories(destDir); @@ -683,4 +401,33 @@ private Path unzip(Path zipPath, Path destDir) throws IOException return firstExecutable != null ? firstExecutable : destDir; } + private String readProcessOutput(Process p) throws IOException + { + try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()))) + { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) + sb.append(line).append(System.lineSeparator()); + return sb.toString(); + } + } + + private String parseVolumePath(InputStream mountOut) throws IOException + { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(mountOut))) + { + String line; + while ((line = reader.readLine()) != null) + { + if (line.contains("/Volumes/")) //$NON-NLS-1$ + { + for (String part : line.split("\t")) //$NON-NLS-1$ + if (part.startsWith("/Volumes/")) //$NON-NLS-1$ + return part.trim(); + } + } + } + return null; + } } diff --git a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/EimLaunchService.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/EimLaunchService.java new file mode 100644 index 000000000..8d8bf114f --- /dev/null +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/EimLaunchService.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright 2025 Espressif Systems (Shanghai) PTE LTD. All rights reserved. + * Use is subject to license terms. + *******************************************************************************/ +package com.espressif.idf.core.tools.launch; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.console.MessageConsoleStream; + +import com.espressif.idf.core.tools.launch.strategies.EimLauncherStrategy; + +/** + * Service for launching EIM using appropriate strategy + * @author Ali Azam Rana + * + */ +public final class EimLaunchService +{ + private final EimLauncherStrategy strategy; + + public EimLaunchService(Display display, MessageConsoleStream standardConsoleStream, + MessageConsoleStream errorConsoleStream) + { + this.strategy = EimLauncherFactory.create(display, standardConsoleStream, errorConsoleStream); + } + + public LaunchResult launch(String eimPath) 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); + } + + public IStatus waitForExit(LaunchResult launchResult, IProgressMonitor monitor) + { + return strategy.waitForExit(launchResult, monitor); + } +} diff --git a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/EimLauncherFactory.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/EimLauncherFactory.java new file mode 100644 index 000000000..7074ffec6 --- /dev/null +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/EimLauncherFactory.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright 2025 Espressif Systems (Shanghai) PTE LTD. All rights reserved. + * Use is subject to license terms. + *******************************************************************************/ +package com.espressif.idf.core.tools.launch; + +import org.eclipse.core.runtime.Platform; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.console.MessageConsoleStream; + +import com.espressif.idf.core.tools.launch.strategies.EimLauncherStrategy; +import com.espressif.idf.core.tools.launch.strategies.LinuxEimLauncherStrategy; +import com.espressif.idf.core.tools.launch.strategies.MacOsEimLauncherStrategy; +import com.espressif.idf.core.tools.launch.strategies.WindowsEimLauncherStrategy; + +/** + * Factory for creating EIM launcher strategies based on OS + * @author Ali Azam Rana + */ +public final class EimLauncherFactory +{ + private EimLauncherFactory() + { + // utility + } + + public static EimLauncherStrategy create(Display display, MessageConsoleStream standardConsoleStream, + MessageConsoleStream errorConsoleStream) + { + String os = Platform.getOS(); + + if (Platform.OS_MACOSX.equals(os)) + { + return new MacOsEimLauncherStrategy(display, standardConsoleStream, errorConsoleStream); + } + if (Platform.OS_WIN32.equals(os)) + { + return new WindowsEimLauncherStrategy(display, standardConsoleStream, errorConsoleStream); + } + if (Platform.OS_LINUX.equals(os)) + { + return new LinuxEimLauncherStrategy(display, standardConsoleStream, errorConsoleStream); + } + + throw new UnsupportedOperationException("Unsupported OS: " + os); //$NON-NLS-1$ + } +} diff --git a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/LaunchResult.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/LaunchResult.java new file mode 100644 index 000000000..0d7423b82 --- /dev/null +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/LaunchResult.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * Copyright 2025 Espressif Systems (Shanghai) PTE LTD. All rights reserved. + * Use is subject to license terms. + *******************************************************************************/ +package com.espressif.idf.core.tools.launch; + +import java.util.OptionalLong; + +/** + * Launch result abstraction so the rest of the code doesn't care how the process + * is identified (PID on Win/Linux, PID or execPath polling on macOS, etc). + * + * @author Ali Azam Rana + */ +public final class LaunchResult +{ + private final OptionalLong pid; + private final String execPath; + private final String details; + + private LaunchResult(OptionalLong pid, String execPath, String details) + { + this.pid = pid; + this.execPath = execPath; + this.details = details; + } + + public static LaunchResult ofPid(long pid, String execPath, String details) + { + return new LaunchResult(OptionalLong.of(pid), execPath, details); + } + + public static LaunchResult ofNoPid(String execPath, String details) + { + return new LaunchResult(OptionalLong.empty(), execPath, details); + } + + public OptionalLong pid() + { + return pid; + } + + /** + * Platform-specific executable path (e.g. .../Contents/MacOS/eim on macOS) or + * the launched binary path on Linux/Windows. + */ + public String execPath() + { + return execPath; + } + + /** + * Debug info: launcher output, command used, etc. + */ + public String details() + { + return details; + } +} diff --git a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/ProcessUtils.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/ProcessUtils.java new file mode 100644 index 000000000..130265a47 --- /dev/null +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/ProcessUtils.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * Copyright 2025 Espressif Systems (Shanghai) PTE LTD. All rights reserved. + * Use is subject to license terms. + *******************************************************************************/ +package com.espressif.idf.core.tools.launch; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; + +/** + * Utility class for process-related operations + * + * @author Ali Azam Rana + */ +public final class ProcessUtils +{ + private ProcessUtils() + { + // utility + } + + public static String readAll(InputStream in) throws IOException + { + try (BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) + { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) + { + sb.append(line).append('\n'); + } + return sb.toString().trim(); + } + } + + public static Long parseFirstLongLine(String output) + { + if (output == null || output.isEmpty()) + return null; + + for (String line : output.split("\n")) //$NON-NLS-1$ + { + String t = line.trim(); + if (t.matches("\\d+")) //$NON-NLS-1$ + { + return Long.valueOf(t); + } + } + return null; + } + + public static String bashSingleQuote(String input) + { + return "'" + input.replace("'", "'\"'\"'") + "'"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ + } +} diff --git a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/ProcessWaiter.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/ProcessWaiter.java new file mode 100644 index 000000000..88203cba9 --- /dev/null +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/ProcessWaiter.java @@ -0,0 +1,104 @@ +/******************************************************************************* + * Copyright 2025 Espressif Systems (Shanghai) PTE LTD. All rights reserved. + * Use is subject to license terms. + *******************************************************************************/ +package com.espressif.idf.core.tools.launch; + +import java.io.IOException; +import java.util.OptionalLong; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; + +import com.espressif.idf.core.logging.Logger; + +/** + * Utility class for waiting for process exit + * @author Ali Azam Rana + */ +public final class ProcessWaiter +{ + private ProcessWaiter() + { + // utility + } + + public static IStatus waitForExitByPid(OptionalLong pidOpt, IProgressMonitor monitor) + { + if (pidOpt == null || pidOpt.isEmpty()) + return Status.OK_STATUS; + + long pid = pidOpt.getAsLong(); + if (pid <= 0) + return Status.error("Invalid PID: " + pid); //$NON-NLS-1$ + + try + { + while (!monitor.isCanceled() && ProcessHandle.of(pid).map(ProcessHandle::isAlive).orElse(false)) + { + Thread.sleep(1000); + } + return monitor.isCanceled() ? Status.CANCEL_STATUS : Status.OK_STATUS; + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + return Status.CANCEL_STATUS; + } + catch (Exception e) + { + Logger.log(e); + return Status.error(e.getMessage()); + } + } + + /** + * Fallback waiter (macOS): poll for a process matching execPath using pgrep. + * This avoids relying on System Events / Accessibility permissions for PID lookup. + */ + public static IStatus waitForExitByExecPath(String execPath, IProgressMonitor monitor) + { + if (execPath == null || execPath.isBlank()) + return Status.error("execPath is null/blank"); //$NON-NLS-1$ + + try + { + while (!monitor.isCanceled() && isAnyProcessMatchingExecPath(execPath)) + { + Thread.sleep(1000); + } + return monitor.isCanceled() ? Status.CANCEL_STATUS : Status.OK_STATUS; + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + return Status.CANCEL_STATUS; + } + catch (Exception e) + { + Logger.log(e); + return Status.error(e.getMessage()); + } + } + + private static boolean isAnyProcessMatchingExecPath(String execPath) throws IOException, InterruptedException + { + // pgrep -f searches the full command line; quote it defensively for sh -lc + String q = ProcessUtils.bashSingleQuote(execPath); + String cmd = "pgrep -f " + q + " >/dev/null 2>&1; echo $?"; //$NON-NLS-1$ //$NON-NLS-2$ + + Process p = new ProcessBuilder("bash", "-lc", cmd) //$NON-NLS-1$ //$NON-NLS-2$ + .redirectErrorStream(true).start(); + + int exit = p.waitFor(); + String out = ProcessUtils.readAll(p.getInputStream()); + + // If bash failed, treat as "not running" to avoid infinite waits. + if (exit != 0 && (out == null || out.isBlank())) + return false; + + // echo $? prints 0 if pgrep found something + return out.trim().endsWith("0"); //$NON-NLS-1$ + } +} diff --git a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/AbstractLoggingLauncherStrategy.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/AbstractLoggingLauncherStrategy.java new file mode 100644 index 000000000..9ffc05d88 --- /dev/null +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/AbstractLoggingLauncherStrategy.java @@ -0,0 +1,65 @@ +/******************************************************************************* + * Copyright 2025 Espressif Systems (Shanghai) PTE LTD. All rights reserved. + * Use is subject to license terms. + *******************************************************************************/ +package com.espressif.idf.core.tools.launch.strategies; + +import java.io.IOException; + +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.console.MessageConsoleStream; + +import com.espressif.idf.core.logging.Logger; + +/** + * Abstract class for logging used by strategies launching EIM + * @author Ali Azam Rana + * + */ +public abstract class AbstractLoggingLauncherStrategy implements EimLauncherStrategy +{ + protected final Display display; + protected final MessageConsoleStream standardConsoleStream; + protected final MessageConsoleStream errorConsoleStream; + + protected AbstractLoggingLauncherStrategy(Display display, MessageConsoleStream standardConsoleStream, + MessageConsoleStream errorConsoleStream) + { + this.display = display; + this.standardConsoleStream = standardConsoleStream; + this.errorConsoleStream = errorConsoleStream; + } + + protected void logMessage(String message) + { + display.asyncExec(() -> { + try + { + standardConsoleStream.write(message); + } + catch (IOException e) + { + Logger.log(e); + logError(e.getMessage()); + } + }); + + Logger.log(message); + } + + protected void logError(String message) + { + display.asyncExec(() -> { + try + { + errorConsoleStream.write(message); + } + catch (IOException e) + { + Logger.log(e); + } + }); + + Logger.log(message); + } +} diff --git a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/EimLauncherStrategy.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/EimLauncherStrategy.java new file mode 100644 index 000000000..402f03f69 --- /dev/null +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/EimLauncherStrategy.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright 2025 Espressif Systems (Shanghai) PTE LTD. All rights reserved. + * Use is subject to license terms. + *******************************************************************************/ +package com.espressif.idf.core.tools.launch.strategies; + +import java.io.IOException; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; + +import com.espressif.idf.core.tools.launch.LaunchResult; + +/** + * Strategy interface for launching EIM + * @author Ali Azam Rana + * * + */ +public interface EimLauncherStrategy +{ + LaunchResult launch(String eimPath) throws IOException; + + IStatus waitForExit(LaunchResult launchResult, IProgressMonitor monitor); +} diff --git a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/LinuxEimLauncherStrategy.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/LinuxEimLauncherStrategy.java new file mode 100644 index 000000000..299aca87a --- /dev/null +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/LinuxEimLauncherStrategy.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * Copyright 2025 Espressif Systems (Shanghai) PTE LTD. All rights reserved. + * Use is subject to license terms. + *******************************************************************************/ +package com.espressif.idf.core.tools.launch.strategies; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.console.MessageConsoleStream; +import com.espressif.idf.core.tools.launch.LaunchResult; +import com.espressif.idf.core.tools.launch.ProcessUtils; +import com.espressif.idf.core.tools.launch.ProcessWaiter; + +import com.espressif.idf.core.logging.Logger; + +/** + * Linux EIM launcher strategy + * @author Ali Azam Rana + */ +public final class LinuxEimLauncherStrategy extends AbstractLoggingLauncherStrategy +{ + public LinuxEimLauncherStrategy(Display display, MessageConsoleStream standardConsoleStream, + MessageConsoleStream errorConsoleStream) + { + super(display, standardConsoleStream, errorConsoleStream); + } + + @Override + public LaunchResult launch(String eimPath) 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$ + + List command = List.of("bash", "-lc", bashCmd); //$NON-NLS-1$ //$NON-NLS-2$ + Process launcher = new ProcessBuilder(command).redirectErrorStream(true).start(); + + String out = ProcessUtils.readAll(launcher.getInputStream()); + Long pid = ProcessUtils.parseFirstLongLine(out); + + if (pid == null) + { + Logger.log("Linux 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); + } + + @Override + public IStatus waitForExit(LaunchResult launchResult, IProgressMonitor monitor) + { + return ProcessWaiter.waitForExitByPid(launchResult.pid(), monitor); + } +} 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 new file mode 100644 index 000000000..9632e34b0 --- /dev/null +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/MacOsEimLauncherStrategy.java @@ -0,0 +1,179 @@ +/******************************************************************************* + * Copyright 2025 Espressif Systems (Shanghai) PTE LTD. All rights reserved. + * Use is subject to license terms. + *******************************************************************************/ +package com.espressif.idf.core.tools.launch.strategies; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.console.MessageConsoleStream; + +import com.espressif.idf.core.logging.Logger; +import com.espressif.idf.core.tools.launch.LaunchResult; +import com.espressif.idf.core.tools.launch.ProcessUtils; +import com.espressif.idf.core.tools.launch.ProcessWaiter; + +/** + * macOS EIM launcher strategy using AppleScript and execPath polling fallback + * @author Ali Azam Rana + */ +public final class MacOsEimLauncherStrategy extends AbstractLoggingLauncherStrategy +{ + /** + * AppleScript tries to: + * - open the app bundle + * - poll System Events for a process with the same bundle id + * - return unix id (PID) + * + * If PID is not returned (System Events permission, slow launch, etc), + * caller will fall back to execPath polling. + */ + private static final String MACOS_LAUNCH_AND_PID_APPLESCRIPT = """ + set appPath to system attribute "APP_PATH" + set bundleId to system attribute "BUNDLE_ID" + + do shell script "open -a " & quoted form of appPath + + tell application "System Events" + repeat 300 times + set matches to (application processes whose bundle identifier is bundleId) + if (count of matches) > 0 then + return unix id of (item 1 of matches) + end if + delay 0.1 + end repeat + end tell + + error "PID not found (app may not have launched)" + """; //$NON-NLS-1$ + + public MacOsEimLauncherStrategy(Display display, MessageConsoleStream standardConsoleStream, + MessageConsoleStream errorConsoleStream) + { + super(display, standardConsoleStream, errorConsoleStream); + } + + @Override + public LaunchResult launch(String eimPath) throws IOException + { + String appBundlePath = deriveAppBundlePath(eimPath); + String execPath = deriveExecPath(eimPath, appBundlePath); + String bundleId = readBundleId(appBundlePath); + + 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$ + + Process p = pb.start(); + try (OutputStream stdin = p.getOutputStream()) + { + stdin.write(MACOS_LAUNCH_AND_PID_APPLESCRIPT.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + } + + String out; + int exit; + try + { + exit = p.waitFor(); + out = ProcessUtils.readAll(p.getInputStream()); + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while waiting for osascript", e); //$NON-NLS-1$ + } + + Logger.log("APP_PATH=" + appBundlePath); //$NON-NLS-1$ + 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$ + } + + @Override + public IStatus waitForExit(LaunchResult launchResult, IProgressMonitor monitor) + { + if (launchResult.pid().isPresent()) + { + return ProcessWaiter.waitForExitByPid(launchResult.pid(), monitor); + } + + // Fallback wait (pgrep -f execPath) + return ProcessWaiter.waitForExitByExecPath(launchResult.execPath(), monitor); + } + + private String deriveAppBundlePath(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 p.toString(); + } + p = p.getParent(); + } + + throw new IllegalArgumentException("Cannot derive .app bundle path from: " + eimPath); //$NON-NLS-1$ + } + + private String deriveExecPath(String eimPath, String appBundlePath) + { + // If caller passed the internal binary path, keep it. + if (eimPath != null && eimPath.contains(".app/Contents/MacOS/")) //$NON-NLS-1$ + { + return eimPath; + } + + // Otherwise derive default binary location for waiting. + return Paths.get(appBundlePath, "Contents", "MacOS", "eim").toString(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + + private String readBundleId(String appBundlePath) throws IOException + { + // Prefer PlistBuddy because it reads Info.plist directly (no LaunchServices dependency). + String infoPlist = Paths.get(appBundlePath, "Contents", "Info.plist").toString(); //$NON-NLS-1$ //$NON-NLS-2$ + Process p = new ProcessBuilder("/usr/libexec/PlistBuddy", "-c", "Print :CFBundleIdentifier", infoPlist) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + .redirectErrorStream(true).start(); + + String out; + int exit; + try + { + exit = p.waitFor(); + out = ProcessUtils.readAll(p.getInputStream()); + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while reading Info.plist", e); //$NON-NLS-1$ + } + + if (exit != 0 || out == null || out.isBlank()) + { + throw new IOException("Failed to read CFBundleIdentifier. Output was:\n" + out); //$NON-NLS-1$ + } + + return out.trim(); + } +} diff --git a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/WindowsEimLauncherStrategy.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/WindowsEimLauncherStrategy.java new file mode 100644 index 000000000..8680aef7d --- /dev/null +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/WindowsEimLauncherStrategy.java @@ -0,0 +1,65 @@ +/******************************************************************************* + * Copyright 2025 Espressif Systems (Shanghai) PTE LTD. All rights reserved. + * Use is subject to license terms. + *******************************************************************************/ +package com.espressif.idf.core.tools.launch.strategies; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.console.MessageConsoleStream; + +import com.espressif.idf.core.logging.Logger; +import com.espressif.idf.core.tools.launch.LaunchResult; +import com.espressif.idf.core.tools.launch.ProcessUtils; +import com.espressif.idf.core.tools.launch.ProcessWaiter; + +/** + * Linux EIM launcher strategy + * @author Ali Azam Rana + */ +public final class WindowsEimLauncherStrategy extends AbstractLoggingLauncherStrategy +{ + public WindowsEimLauncherStrategy(Display display, MessageConsoleStream standardConsoleStream, + MessageConsoleStream errorConsoleStream) + { + super(display, standardConsoleStream, errorConsoleStream); + } + + @Override + public LaunchResult launch(String eimPath) 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 powershellCmd = String.format( + "Start-Process -FilePath '%s' -PassThru | Select-Object -ExpandProperty Id", //$NON-NLS-1$ + escapedPathForPowershell); + + List command = List.of("powershell.exe", "-Command", powershellCmd); //$NON-NLS-1$ //$NON-NLS-2$ + Process launcher = new ProcessBuilder(command).redirectErrorStream(true).start(); + + String out = ProcessUtils.readAll(launcher.getInputStream()); + Long pid = ProcessUtils.parseFirstLongLine(out); + + if (pid == null) + { + Logger.log("Windows 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); + } + + @Override + public IStatus waitForExit(LaunchResult launchResult, IProgressMonitor monitor) + { + return ProcessWaiter.waitForExitByPid(launchResult.pid(), monitor); + } +} diff --git a/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EimButtonLaunchListener.java b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EimButtonLaunchListener.java index dd265eed8..3baa44356 100644 --- a/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EimButtonLaunchListener.java +++ b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EimButtonLaunchListener.java @@ -28,6 +28,7 @@ import com.espressif.idf.core.tools.EimLoader; import com.espressif.idf.core.tools.ToolInitializer; import com.espressif.idf.core.tools.exceptions.EimVersionMismatchException; +import com.espressif.idf.core.tools.launch.LaunchResult; import com.espressif.idf.core.tools.vo.EimJson; import com.espressif.idf.ui.UIPlugin; import com.espressif.idf.ui.handlers.EclipseHandler; @@ -87,8 +88,8 @@ protected IStatus run(IProgressMonitor monitor) { try { - long eimPid = eimLoader.launchEim(idfEnvironmentVariables.getEnvValue(IDFEnvironmentVariables.EIM_PATH)); - eimLoader.waitForEimClosure(eimPid, EimButtonLaunchListener.this::refreshAfterEimClose); + var launchResult = eimLoader.launchEimWithResult(idfEnvironmentVariables.getEnvValue(IDFEnvironmentVariables.EIM_PATH)); + eimLoader.waitForEimClosure(launchResult, EimButtonLaunchListener.this::refreshAfterEimClose); } catch (IOException e) { @@ -194,12 +195,12 @@ public void onCompleted(String filePath) } } - long pidEim = -1; + try { idfEnvironmentVariables.addEnvVariable(IDFEnvironmentVariables.EIM_PATH, appToLaunch); - pidEim = eimLoader.launchEim(appToLaunch); - eimLoader.waitForEimClosure(pidEim, EimButtonLaunchListener.this::refreshAfterEimClose); + LaunchResult launchResult = eimLoader.launchEimWithResult(appToLaunch); + eimLoader.waitForEimClosure(launchResult, EimButtonLaunchListener.this::refreshAfterEimClose); } catch (IOException e) { diff --git a/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EspressifToolStartup.java b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EspressifToolStartup.java index eb5687015..9874bb8d9 100644 --- a/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EspressifToolStartup.java +++ b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EspressifToolStartup.java @@ -49,6 +49,8 @@ import com.espressif.idf.ui.tools.manager.EimEditorInput; import com.espressif.idf.ui.tools.watcher.EimJsonUiChangeHandler; +import com.espressif.idf.core.tools.launch.LaunchResult; + /** * Startup class to handle the tools * @@ -376,7 +378,7 @@ public void onCompleted(String filePath) } }); - long eimPid = -1; + LaunchResult launchResult = null; String appToLaunch = filePath; try { @@ -386,7 +388,7 @@ public void onCompleted(String filePath) } idfEnvironmentVariables.addEnvVariable(IDFEnvironmentVariables.EIM_PATH, appToLaunch); - eimPid = eimLoader.launchEim(appToLaunch); + launchResult = eimLoader.launchEimWithResult(appToLaunch); } catch ( IOException @@ -395,7 +397,7 @@ public void onCompleted(String filePath) Logger.log(e); } - eimLoader.waitForEimClosure(eimPid, () -> { + eimLoader.waitForEimClosure(launchResult, () -> { if (toolInitializer.isOldEspIdfConfigPresent() && !toolInitializer.isOldConfigExported()) { Logger.log("Old configuration found and not converted"); From f1f9a8fc1cbbe94dedc3bec11bc784f57aead91a Mon Sep 17 00:00:00 2001 From: Ali Azam Rana <85216275+alirana01@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:34:24 +0100 Subject: [PATCH 3/3] fix for welcome page --- bundles/com.espressif.idf.ui/META-INF/MANIFEST.MF | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bundles/com.espressif.idf.ui/META-INF/MANIFEST.MF b/bundles/com.espressif.idf.ui/META-INF/MANIFEST.MF index 2bc59e837..af5959f68 100644 --- a/bundles/com.espressif.idf.ui/META-INF/MANIFEST.MF +++ b/bundles/com.espressif.idf.ui/META-INF/MANIFEST.MF @@ -29,7 +29,8 @@ Require-Bundle: org.eclipse.core.runtime, org.eclipse.ltk.ui.refactoring, org.eclipse.ltk.core.refactoring, org.eclipse.tools.templates.core, - org.eclipse.tools.templates.ui + org.eclipse.tools.templates.ui, + org.eclipse.ui.intro Automatic-Module-Name: com.espressif.idf.ui Bundle-ActivationPolicy: lazy Bundle-RequiredExecutionEnvironment: JavaSE-17