CommandExecutor.java

  1. /*
  2.  * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com>
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */

  10. package org.eclipse.jgit.internal.diffmergetool;

  11. import java.io.File;
  12. import java.io.FileOutputStream;
  13. import java.io.IOException;
  14. import java.io.OutputStream;
  15. import java.nio.file.Files;
  16. import java.nio.file.Path;
  17. import java.nio.file.Paths;
  18. import java.util.Arrays;
  19. import java.util.Map;

  20. import org.eclipse.jgit.errors.NoWorkTreeException;
  21. import org.eclipse.jgit.util.FS;
  22. import org.eclipse.jgit.util.FS.ExecutionResult;
  23. import org.eclipse.jgit.util.FS_POSIX;
  24. import org.eclipse.jgit.util.FS_Win32;
  25. import org.eclipse.jgit.util.FS_Win32_Cygwin;
  26. import org.eclipse.jgit.util.StringUtils;
  27. import org.eclipse.jgit.util.SystemReader;

  28. /**
  29.  * Runs a command with help of FS.
  30.  */
  31. public class CommandExecutor {

  32.     private FS fs;

  33.     private boolean checkExitCode;

  34.     private File commandFile;

  35.     private boolean useMsys2;

  36.     /**
  37.      * @param fs
  38.      *            the file system
  39.      * @param checkExitCode
  40.      *            should the exit code be checked for errors ?
  41.      */
  42.     public CommandExecutor(FS fs, boolean checkExitCode) {
  43.         this.fs = fs;
  44.         this.checkExitCode = checkExitCode;
  45.     }

  46.     /**
  47.      * @param command
  48.      *            the command string
  49.      * @param workingDir
  50.      *            the working directory
  51.      * @param env
  52.      *            the environment
  53.      * @return the execution result
  54.      * @throws ToolException
  55.      * @throws InterruptedException
  56.      * @throws IOException
  57.      */
  58.     public ExecutionResult run(String command, File workingDir,
  59.             Map<String, String> env)
  60.             throws ToolException, IOException, InterruptedException {
  61.         String[] commandArray = createCommandArray(command);
  62.         try {
  63.             ProcessBuilder pb = fs.runInShell(commandArray[0],
  64.                     Arrays.copyOfRange(commandArray, 1, commandArray.length));
  65.             pb.directory(workingDir);
  66.             Map<String, String> envp = pb.environment();
  67.             if (env != null) {
  68.                 envp.putAll(env);
  69.             }
  70.             ExecutionResult result = fs.execute(pb, null);
  71.             int rc = result.getRc();
  72.             if (rc != 0) {
  73.                 boolean execError = isCommandExecutionError(rc);
  74.                 if (checkExitCode || execError) {
  75.                     throw new ToolException(
  76.                             "JGit: tool execution return code: " + rc + "\n" //$NON-NLS-1$ //$NON-NLS-2$
  77.                                     + "checkExitCode: " + checkExitCode + "\n" //$NON-NLS-1$ //$NON-NLS-2$
  78.                                     + "execError: " + execError + "\n" //$NON-NLS-1$ //$NON-NLS-2$
  79.                                     + "stderr: \n" //$NON-NLS-1$
  80.                                     + new String(
  81.                                             result.getStderr().toByteArray(),
  82.                                             SystemReader.getInstance()
  83.                                                     .getDefaultCharset()),
  84.                             result, execError);
  85.                 }
  86.             }
  87.             return result;
  88.         } finally {
  89.             deleteCommandArray();
  90.         }
  91.     }

  92.     /**
  93.      * @param path
  94.      *            the executable path
  95.      * @param workingDir
  96.      *            the working directory
  97.      * @param env
  98.      *            the environment
  99.      * @return the execution result
  100.      * @throws ToolException
  101.      * @throws InterruptedException
  102.      * @throws IOException
  103.      */
  104.     public boolean checkExecutable(String path, File workingDir,
  105.             Map<String, String> env)
  106.             throws ToolException, IOException, InterruptedException {
  107.         checkUseMsys2(path);
  108.         String command = null;
  109.         if (fs instanceof FS_Win32 && !useMsys2) {
  110.             Path p = Paths.get(path);
  111.             // Win32 (and not cygwin or MSYS2) where accepts only command / exe
  112.             // name as parameter
  113.             // so check if exists and executable in this case
  114.             if (p.isAbsolute() && Files.isExecutable(p)) {
  115.                 return true;
  116.             }
  117.             // try where command for all other cases
  118.             command = "where " + ExternalToolUtils.quotePath(path); //$NON-NLS-1$
  119.         } else {
  120.             command = "which " + ExternalToolUtils.quotePath(path); //$NON-NLS-1$
  121.         }
  122.         boolean available = true;
  123.         try {
  124.             ExecutionResult rc = run(command, workingDir, env);
  125.             if (rc.getRc() != 0) {
  126.                 available = false;
  127.             }
  128.         } catch (IOException | InterruptedException | NoWorkTreeException
  129.                 | ToolException e) {
  130.             // no op: is true to not hide possible tools from user
  131.         }
  132.         return available;
  133.     }

  134.     private void deleteCommandArray() {
  135.         deleteCommandFile();
  136.     }

  137.     private String[] createCommandArray(String command)
  138.             throws ToolException, IOException {
  139.         String[] commandArray = null;
  140.         checkUseMsys2(command);
  141.         createCommandFile(command);
  142.         if (fs instanceof FS_POSIX) {
  143.             commandArray = new String[1];
  144.             commandArray[0] = commandFile.getCanonicalPath();
  145.         } else if (fs instanceof FS_Win32) {
  146.             if (useMsys2) {
  147.                 commandArray = new String[3];
  148.                 commandArray[0] = "bash.exe"; //$NON-NLS-1$
  149.                 commandArray[1] = "-c"; //$NON-NLS-1$
  150.                 commandArray[2] = commandFile.getCanonicalPath().replace("\\", //$NON-NLS-1$
  151.                         "/"); //$NON-NLS-1$
  152.             } else {
  153.                 commandArray = new String[1];
  154.                 commandArray[0] = commandFile.getCanonicalPath();
  155.             }
  156.         } else if (fs instanceof FS_Win32_Cygwin) {
  157.             commandArray = new String[1];
  158.             commandArray[0] = commandFile.getCanonicalPath().replace("\\", "/"); //$NON-NLS-1$ //$NON-NLS-2$
  159.         } else {
  160.             throw new ToolException(
  161.                     "JGit: file system not supported: " + fs.toString()); //$NON-NLS-1$
  162.         }
  163.         return commandArray;
  164.     }

  165.     private void checkUseMsys2(String command) {
  166.         useMsys2 = false;
  167.         String useMsys2Str = System.getProperty("jgit.usemsys2bash"); //$NON-NLS-1$
  168.         if (!StringUtils.isEmptyOrNull(useMsys2Str)) {
  169.             if (useMsys2Str.equalsIgnoreCase("auto")) { //$NON-NLS-1$
  170.                 useMsys2 = command.contains(".sh"); //$NON-NLS-1$
  171.             } else {
  172.                 useMsys2 = Boolean.parseBoolean(useMsys2Str);
  173.             }
  174.         }
  175.     }

  176.     private void createCommandFile(String command)
  177.             throws ToolException, IOException {
  178.         String fileExtension = null;
  179.         if (useMsys2 || fs instanceof FS_POSIX
  180.                 || fs instanceof FS_Win32_Cygwin) {
  181.             fileExtension = ".sh"; //$NON-NLS-1$
  182.         } else if (fs instanceof FS_Win32) {
  183.             fileExtension = ".cmd"; //$NON-NLS-1$
  184.             command = "@echo off" + System.lineSeparator() + command //$NON-NLS-1$
  185.                     + System.lineSeparator() + "exit /B %ERRORLEVEL%"; //$NON-NLS-1$
  186.         } else {
  187.             throw new ToolException(
  188.                     "JGit: file system not supported: " + fs.toString()); //$NON-NLS-1$
  189.         }
  190.         commandFile = File.createTempFile(".__", //$NON-NLS-1$
  191.                 "__jgit_tool" + fileExtension); //$NON-NLS-1$
  192.         try (OutputStream outStream = new FileOutputStream(commandFile)) {
  193.             byte[] strToBytes = command
  194.                     .getBytes(SystemReader.getInstance().getDefaultCharset());
  195.             outStream.write(strToBytes);
  196.             outStream.close();
  197.         }
  198.         commandFile.setExecutable(true);
  199.     }

  200.     private void deleteCommandFile() {
  201.         if (commandFile != null && commandFile.exists()) {
  202.             commandFile.delete();
  203.         }
  204.     }

  205.     private boolean isCommandExecutionError(int rc) {
  206.         if (useMsys2 || fs instanceof FS_POSIX
  207.                 || fs instanceof FS_Win32_Cygwin) {
  208.             // 126: permission for executing command denied
  209.             // 127: command not found
  210.             if ((rc == 126) || (rc == 127)) {
  211.                 return true;
  212.             }
  213.         }
  214.         else if (fs instanceof FS_Win32) {
  215.             // 9009, 0x2331: Program is not recognized as an internal or
  216.             // external command, operable program or batch file. Indicates that
  217.             // command, application name or path has been misspelled when
  218.             // configuring the Action.
  219.             if (rc == 9009) {
  220.                 return true;
  221.             }
  222.         }
  223.         return false;
  224.     }

  225. }