package com.github.masahirosuzuka.PhoneGapIntelliJPlugin.commandLine; import com.intellij.execution.ExecutionException; import com.intellij.execution.configurations.GeneralCommandLine; import com.intellij.execution.configurations.GeneralCommandLine.ParentEnvironmentType; import com.intellij.execution.process.*; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.text.StringUtil; import com.intellij.util.ArrayUtil; import com.intellij.util.Function; import com.intellij.util.containers.ContainerUtil; import org.apache.commons.codec.Charsets; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; import java.util.*; import java.util.concurrent.TimeUnit; import static com.intellij.openapi.util.text.StringUtil.contains; import static com.intellij.util.containers.ContainerUtil.concat; import static com.intellij.util.containers.ContainerUtil.newArrayList; public class PhoneGapCommandLine { private static final Logger LOGGER = Logger.getInstance(PhoneGapCommandLine.class); public static final String INFO_PHONEGAP = "the following plugins are installed"; public static final String PLATFORM_PHONEGAP = "phonegap"; public static final String PLATFORM_CORDOVA = "cordova"; public static final String PLATFORM_IONIC = "ionic"; public static final String COMMAND_RUN = "run"; public static final String COMMAND_PREPARE = "prepare"; public static final String COMMAND_EMULATE = "emulate"; public static final String COMMAND_SERVE = "serve"; public static final String COMMAND_REMOTE_RUN = "remote run"; public static final String COMMAND_REMOTE_BUILD = "remote build"; public static final long PROCESS_TIMEOUT = TimeUnit.SECONDS.toMillis(120); @Nullable private final String myWorkDir; public boolean isPassParentEnv() { return myPassParentEnv; } public Map<String, String> getEnv() { return myEnv; } @Nullable public String getWorkDir() { return myWorkDir; } @NotNull public String getPath() { return myPath; } @NotNull private final String myPath; @Nullable private String version; private boolean myIsCorrect = true; public void setPassParentEnv(boolean passParentEnv) { myPassParentEnv = passParentEnv; } public void setEnv(Map<String, String> env) { myEnv = env; } private boolean myPassParentEnv = true; private Map<String, String> myEnv = ContainerUtil.newHashMap(); private final String myOptions; public static final Function<String, String> REMOVE_QUOTE_AND_TRIM = s -> s.replace("'", "").trim(); public PhoneGapCommandLine(@NotNull String path, @Nullable String dir, boolean isPassEnv, Map<String, String> env) { myWorkDir = dir; myPath = path; myEnv = env; myPassParentEnv = isPassEnv; myOptions = null; try { version = getInnerVersion(myPath, "--version").replace("\"", "").trim(); } catch (Exception e) { version = null; LOGGER.debug(e.getMessage(), e); myIsCorrect = false; } } public PhoneGapCommandLine(@NotNull String path, @Nullable String dir, @Nullable String options) { myWorkDir = dir; myPath = path; myOptions = options; try { version = getInnerVersion(myPath, "--version").replace("\"", "").trim(); } catch (Exception e) { version = null; LOGGER.debug(e.getMessage(), e); myIsCorrect = false; } } public PhoneGapCommandLine(@NotNull String path, @Nullable String dir) { this(path, dir, null); } public ProcessOutput platformAdd(@NotNull String platform) throws ExecutionException { String trimmedPlatform = platform.trim(); ProcessOutput output = executeAndGetOut(new String[]{myPath, "platform", "add", trimmedPlatform}); String message = "Platform " + trimmedPlatform + " already added"; if (output.getExitCode() != 0 && (contains(output.getStderr(), message) || contains(output.getStdout(), message))) { return new ProcessOutput(0); } return output; } public boolean isCorrectExecutable() { return myIsCorrect; } public String version() { return version; } public void pluginAdd(String fqn) { executeVoidCommand(myPath, "plugin", "add", fqn); } public void pluginRemove(String fqn) { executeVoidCommand(myPath, "plugin", "remove", fqn); } public ProcessOutput pluginListRaw() throws ExecutionException { return executeAndGetOut(new String[]{myPath, "plugin", "list"}); } public String getPlatformName() { return isIonic() ? "Ionic" : "PhoneGap/Cordova"; } public boolean isOld() { if (isIonic()) return false; if (StringUtil.isEmpty(version) || !Character.isDigit(version.charAt(0))) return false; try { String[] split = version.split("\\."); int first = Integer.parseInt(split[0]); if (first > 3) return false; if (first == 3) { return (split.length < 2 || Integer.parseInt(split[1]) < 5); } return first < 3; } catch (RuntimeException e) { LOGGER.debug(e.getMessage(), e); } return false; } @NotNull public List<String> pluginList() { try { String out = executeAndReturnResult(myPath, "plugin", "list").trim(); return parsePluginList(out); } catch (RuntimeException e) { LOGGER.debug(e.getMessage(), e); return Collections.emptyList(); } } private OSProcessHandler serve(String extraArg) throws ExecutionException { GeneralCommandLine commandLine = new GeneralCommandLine(concat(newArrayList(myPath, "serve"), parseArgs(extraArg))); commandLine.withWorkDirectory(myWorkDir); return new KillableColoredProcessHandler(commandLine, true); } @NotNull public OSProcessHandler runCommand(@NotNull String command, @NotNull String platform, @Nullable String target, @Nullable String extraArgs ) throws ExecutionException { if (COMMAND_RUN.equals(command)) { return executeStandardCommand(platform, target, extraArgs, COMMAND_RUN); } if (COMMAND_EMULATE.equals(command)) { return emulate(platform, target, extraArgs); } if (COMMAND_SERVE.equals(command)) { return serve(extraArgs); } if (COMMAND_REMOTE_RUN.equals(command)) { return remoteRun(platform, extraArgs); } if (COMMAND_REMOTE_BUILD.equals(command)) { return remoteBuild(platform, extraArgs); } if (COMMAND_PREPARE.equals(command)) { return executeStandardCommand(platform, target, extraArgs, COMMAND_PREPARE); } throw new IllegalStateException("Unsupported command"); } private OSProcessHandler remoteRun(@NotNull String platform, @Nullable String extraArg) throws ExecutionException { return createProcessHandler(concat(newArrayList(myPath, "remote", "run", platform), parseArgs(extraArg))); } private OSProcessHandler remoteBuild(@NotNull String platform, @Nullable String extraArg) throws ExecutionException { return createProcessHandler(concat(newArrayList(myPath, "remote", "build", platform), parseArgs(extraArg))); } private OSProcessHandler emulate(@NotNull String platform, @Nullable String target, @Nullable String extraArg) throws ExecutionException { String[] command; if (!StringUtil.isEmpty(target)) { target = target.trim(); command = new String[]{myPath, "run", "--emulator", "--target=" + target, platform}; } else { command = new String[]{myPath, "run", "--emulator", platform}; } return createProcessHandler(concat(newArrayList(command), parseArgs(extraArg))); } private OSProcessHandler executeStandardCommand(@NotNull String platform, @Nullable String target, @Nullable String extraArg, @NotNull String commandToExecute) throws ExecutionException { String[] command; if (!StringUtil.isEmpty(target)) { target = target.trim(); command = new String[]{myPath, commandToExecute, "--target=" + target, platform}; } else { command = new String[]{myPath, commandToExecute, platform}; } return createProcessHandler(concat(newArrayList(command), parseArgs(extraArg))); } public boolean needAddPlatform() { if (!isPhoneGap()) return true; return isPhonegapAfter363(version); } static boolean isPhonegapAfter363(String version) { if (StringUtil.isEmpty(version)) return true; return StringUtil.compareVersionNumbers(version, "3.6.3") >= 0; } public void createNewProject(String name, @Nullable ProgressIndicator indicator) throws Exception { String command = isIonic() ? "start" : "create"; if (myOptions == null) { executeVoidCommand(indicator, myPath, command, name); } else { String[] resultCommand = ArrayUtil.mergeArrays(new String[]{myPath, command, name, myOptions}); executeVoidCommand(indicator, resultCommand); } } private boolean isPhoneGap() { assert myWorkDir != null; Boolean isPhoneGapByName = isPhoneGapExecutableByPath(myPath); if (isPhoneGapByName != null) return isPhoneGapByName; String s = executeAndReturnResult(myPath); return s.contains(PLATFORM_PHONEGAP); } /** * @param path * @return true - phonegap / false - not phonegap / null - cannot detect */ @Nullable public static Boolean isPhoneGapExecutableByPath(@Nullable String path) { if (StringUtil.isEmpty(path)) return false; File file = new File(path); if (!file.exists()) return false; if (file.getName().contains(PLATFORM_IONIC)) return false; if (file.getName().contains(PLATFORM_CORDOVA)) return false; if (file.getName().contains(PLATFORM_PHONEGAP)) return true; return null; } private boolean isIonic() { File file = new File(myPath); if (file.getName().contains(PLATFORM_IONIC)) return true; if (file.getName().contains(PLATFORM_PHONEGAP)) return false; if (file.getName().contains(PLATFORM_CORDOVA)) return false; if (myWorkDir != null) { String s = executeAndReturnResult(myPath); return s.contains(PLATFORM_IONIC); } return false; } static List<String> parsePluginList(String out) { if (StringUtil.isEmpty(out) || contains(out.toLowerCase(Locale.getDefault()), "no plugins")) { return newArrayList(); } if (out.startsWith("[") && out.endsWith("]")) { out = out.substring(1, out.length() - 1); return ContainerUtil.map(out.split(","), REMOVE_QUOTE_AND_TRIM); } if (out.startsWith("[")) { out = out.replaceAll("\\[(.*?)\\]", ""); } List<String> plugins = ContainerUtil.map(out.split("\n"), StringUtil.TRIMMER); String item = ContainerUtil.getFirstItem(plugins); if (item != null && item.contains(INFO_PHONEGAP)) { plugins = plugins.subList(1, plugins.size()); } return plugins; } private void executeVoidCommand(final String... command) { executeVoidCommand(null, command); } private void executeVoidCommand(ProgressIndicator indicator, final String... command) { try { ProcessOutput output = executeAndGetOut(indicator, command); if (output.getExitCode() > 0) { throw new RuntimeException("Command error: " + output.getStderr()); } } catch (Exception e) { LOGGER.debug(e.getMessage(), e); throw new RuntimeException("Select correct executable path", e); } } private String getInnerVersion(String... command) { try { final ProcessOutput output = executeAndGetOut(command); String stderr = output.getStderr(); if (output.getExitCode() > 0) { throw new RuntimeException("Command error: " + stderr); } String stdout = output.getStdout(); if (StringUtil.isEmpty(stdout) && !StringUtil.isEmpty(stderr)) { return stderr; } return stdout; } catch (Exception e) { throw new RuntimeException(e.getMessage(), e); } } private String executeAndReturnResult(String... command) { try { final ProcessOutput output = executeAndGetOut(command); if (output.getExitCode() > 0) { throw new RuntimeException("Command error: " + output.getStderr()); } return output.getStdout(); } catch (Exception e) { throw new RuntimeException(e.getMessage(), e); } } private ProcessOutput executeAndGetOut(String[] command) throws ExecutionException { return executeAndGetOut(null, command); } private ProcessOutput executeAndGetOut(@Nullable ProgressIndicator indicator, String[] command) throws ExecutionException { final GeneralCommandLine commandLine = new GeneralCommandLine(command); commandLine.withWorkDirectory(myWorkDir); commandLine.withParentEnvironmentType(myPassParentEnv ? ParentEnvironmentType.CONSOLE : ParentEnvironmentType.NONE); commandLine.withEnvironment(myEnv); commandLine.setCharset(Charsets.UTF_8); OSProcessHandler processHandler = new ColoredProcessHandler(commandLine); final ProcessOutput output = new ProcessOutput(); processHandler.addProcessListener(new ProcessAdapter() { @Override public void onTextAvailable(ProcessEvent event, Key outputType) { if (indicator != null ) { String s = StringUtil.trim(event.getText()); if (!StringUtil.isEmpty(s)) { indicator.setText2(s); } } if (outputType == ProcessOutputTypes.STDERR) { output.appendStderr(event.getText()); } else if (outputType != ProcessOutputTypes.SYSTEM) { output.appendStdout(event.getText()); } } }); processHandler.startNotify(); if (processHandler.waitFor(PROCESS_TIMEOUT)) { output.setExitCode(processHandler.getProcess().exitValue()); } else { processHandler.destroyProcess(); output.setTimeout(); } return output; } private OSProcessHandler createProcessHandler(List<String> commands) throws ExecutionException { return createProcessHandler(ArrayUtil.toStringArray(commands)); } private OSProcessHandler createProcessHandler(String... commands) throws ExecutionException { GeneralCommandLine commandLine = new GeneralCommandLine(commands); commandLine.withWorkDirectory(myWorkDir); return new KillableColoredProcessHandler(commandLine, true); } private static List<String> parseArgs(String paramList) { ArrayList<String> list = newArrayList(); if (StringUtil.isEmpty(paramList)) return list; for (String s : paramList.split(" ")) { String trim = StringUtil.trim(s); if (trim != null && !trim.isEmpty()) { list.add(trim); } } return list; } }