/* * Copyright (C) 2018-2021, Andre Bossert * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at * https://www.eclipse.org/org/documents/edl-v10.php. * * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.internal.diffmergetool; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.Map; import org.eclipse.jgit.errors.NoWorkTreeException; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FS.ExecutionResult; import org.eclipse.jgit.util.FS_POSIX; import org.eclipse.jgit.util.FS_Win32; import org.eclipse.jgit.util.FS_Win32_Cygwin; import org.eclipse.jgit.util.StringUtils; import org.eclipse.jgit.util.SystemReader; /** * Runs a command with help of FS. */ public class CommandExecutor { private FS fs; private boolean checkExitCode; private File commandFile; private boolean useMsys2; /** * @param fs * the file system * @param checkExitCode * should the exit code be checked for errors ? */ public CommandExecutor(FS fs, boolean checkExitCode) { this.fs = fs; this.checkExitCode = checkExitCode; } /** * Run command * * @param command * the command string * @param workingDir * the working directory * @param env * the environment * @return the execution result * @throws ToolException * if a tool raised an error * @throws InterruptedException * if thread was interrupted * @throws IOException * if an IO error occurred */ public ExecutionResult run(String command, File workingDir, Map env) throws ToolException, IOException, InterruptedException { String[] commandArray = createCommandArray(command); try { ProcessBuilder pb = fs.runInShell(commandArray[0], Arrays.copyOfRange(commandArray, 1, commandArray.length)); pb.directory(workingDir); Map envp = pb.environment(); if (env != null) { envp.putAll(env); } ExecutionResult result = fs.execute(pb, null); int rc = result.getRc(); if (rc != 0) { boolean execError = isCommandExecutionError(rc); if (checkExitCode || execError) { throw new ToolException( "JGit: tool execution return code: " + rc + "\n" //$NON-NLS-1$ //$NON-NLS-2$ + "checkExitCode: " + checkExitCode + "\n" //$NON-NLS-1$ //$NON-NLS-2$ + "execError: " + execError + "\n" //$NON-NLS-1$ //$NON-NLS-2$ + "stderr: \n" //$NON-NLS-1$ + new String( result.getStderr().toByteArray(), SystemReader.getInstance() .getDefaultCharset()), result, execError); } } return result; } finally { deleteCommandArray(); } } /** * Check whether executable file is available * * @param path * the executable path * @param workingDir * the working directory * @param env * the environment * @return the execution result * @throws ToolException * if a tool raised an error * @throws InterruptedException * if thread was interrupted * @throws IOException * if an IO error occurred */ public boolean checkExecutable(String path, File workingDir, Map env) throws ToolException, IOException, InterruptedException { checkUseMsys2(path); String command = null; if (fs instanceof FS_Win32 && !useMsys2) { Path p = Paths.get(path); // Win32 (and not cygwin or MSYS2) where accepts only command / exe // name as parameter // so check if exists and executable in this case if (p.isAbsolute() && Files.isExecutable(p)) { return true; } // try where command for all other cases command = "where " + ExternalToolUtils.quotePath(path); //$NON-NLS-1$ } else { command = "which " + ExternalToolUtils.quotePath(path); //$NON-NLS-1$ } boolean available = true; try { ExecutionResult rc = run(command, workingDir, env); if (rc.getRc() != 0) { available = false; } } catch (IOException | InterruptedException | NoWorkTreeException | ToolException e) { // no op: is true to not hide possible tools from user } return available; } private void deleteCommandArray() { deleteCommandFile(); } private String[] createCommandArray(String command) throws ToolException, IOException { String[] commandArray = null; checkUseMsys2(command); createCommandFile(command); if (fs instanceof FS_POSIX) { commandArray = new String[1]; commandArray[0] = commandFile.getCanonicalPath(); } else if (fs instanceof FS_Win32_Cygwin) { commandArray = new String[1]; commandArray[0] = commandFile.getCanonicalPath().replace("\\", "/"); //$NON-NLS-1$ //$NON-NLS-2$ } else if (fs instanceof FS_Win32) { if (useMsys2) { commandArray = new String[3]; commandArray[0] = "bash.exe"; //$NON-NLS-1$ commandArray[1] = "-c"; //$NON-NLS-1$ commandArray[2] = commandFile.getCanonicalPath().replace("\\", //$NON-NLS-1$ "/"); //$NON-NLS-1$ } else { commandArray = new String[1]; commandArray[0] = commandFile.getCanonicalPath(); } } else { throw new ToolException( "JGit: file system not supported: " + fs.toString()); //$NON-NLS-1$ } return commandArray; } private void checkUseMsys2(String command) { useMsys2 = false; String useMsys2Str = System.getProperty("jgit.usemsys2bash"); //$NON-NLS-1$ if (!StringUtils.isEmptyOrNull(useMsys2Str)) { if (useMsys2Str.equalsIgnoreCase("auto")) { //$NON-NLS-1$ useMsys2 = command.contains(".sh"); //$NON-NLS-1$ } else { useMsys2 = Boolean.parseBoolean(useMsys2Str); } } } private void createCommandFile(String command) throws ToolException, IOException { String fileExtension = null; if (useMsys2 || fs instanceof FS_POSIX || fs instanceof FS_Win32_Cygwin) { fileExtension = ".sh"; //$NON-NLS-1$ } else if (fs instanceof FS_Win32) { fileExtension = ".cmd"; //$NON-NLS-1$ command = "@echo off" + System.lineSeparator() + command //$NON-NLS-1$ + System.lineSeparator() + "exit /B %ERRORLEVEL%"; //$NON-NLS-1$ } else { throw new ToolException( "JGit: file system not supported: " + fs.toString()); //$NON-NLS-1$ } commandFile = File.createTempFile(".__", //$NON-NLS-1$ "__jgit_tool" + fileExtension); //$NON-NLS-1$ try (OutputStream outStream = new FileOutputStream(commandFile)) { byte[] strToBytes = command .getBytes(SystemReader.getInstance().getDefaultCharset()); outStream.write(strToBytes); outStream.close(); } commandFile.setExecutable(true); } private void deleteCommandFile() { if (commandFile != null && commandFile.exists()) { commandFile.delete(); } } private boolean isCommandExecutionError(int rc) { if (useMsys2 || fs instanceof FS_POSIX || fs instanceof FS_Win32_Cygwin) { // 126: permission for executing command denied // 127: command not found if ((rc == 126) || (rc == 127)) { return true; } } else if (fs instanceof FS_Win32) { // 9009, 0x2331: Program is not recognized as an internal or // external command, operable program or batch file. Indicates that // command, application name or path has been misspelled when // configuring the Action. if (rc == 9009) { return true; } } return false; } }