View Javadoc
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  
11  package org.eclipse.jgit.internal.diffmergetool;
12  
13  import java.io.File;
14  import java.io.FileOutputStream;
15  import java.io.IOException;
16  import java.io.OutputStream;
17  import java.nio.file.Files;
18  import java.nio.file.Path;
19  import java.nio.file.Paths;
20  import java.util.Arrays;
21  import java.util.Map;
22  
23  import org.eclipse.jgit.errors.NoWorkTreeException;
24  import org.eclipse.jgit.util.FS;
25  import org.eclipse.jgit.util.FS.ExecutionResult;
26  import org.eclipse.jgit.util.FS_POSIX;
27  import org.eclipse.jgit.util.FS_Win32;
28  import org.eclipse.jgit.util.FS_Win32_Cygwin;
29  import org.eclipse.jgit.util.StringUtils;
30  
31  /**
32   * Runs a command with help of FS.
33   */
34  public class CommandExecutor {
35  
36  	private FS fs;
37  
38  	private boolean checkExitCode;
39  
40  	private File commandFile;
41  
42  	private boolean useMsys2;
43  
44  	/**
45  	 * @param fs
46  	 *            the file system
47  	 * @param checkExitCode
48  	 *            should the exit code be checked for errors ?
49  	 */
50  	public CommandExecutor(FS fs, boolean checkExitCode) {
51  		this.fs = fs;
52  		this.checkExitCode = checkExitCode;
53  	}
54  
55  	/**
56  	 * @param command
57  	 *            the command string
58  	 * @param workingDir
59  	 *            the working directory
60  	 * @param env
61  	 *            the environment
62  	 * @return the execution result
63  	 * @throws ToolException
64  	 * @throws InterruptedException
65  	 * @throws IOException
66  	 */
67  	public ExecutionResult run(String command, File workingDir,
68  			Map<String, String> env)
69  			throws ToolException, IOException, InterruptedException {
70  		String[] commandArray = createCommandArray(command);
71  		try {
72  			ProcessBuilder pb = fs.runInShell(commandArray[0],
73  					Arrays.copyOfRange(commandArray, 1, commandArray.length));
74  			pb.directory(workingDir);
75  			Map<String, String> envp = pb.environment();
76  			if (env != null) {
77  				envp.putAll(env);
78  			}
79  			ExecutionResult result = fs.execute(pb, null);
80  			int rc = result.getRc();
81  			if (rc != 0) {
82  				boolean execError = isCommandExecutionError(rc);
83  				if (checkExitCode || execError) {
84  					throw new ToolException(
85  							"JGit: tool execution return code: " + rc + "\n" //$NON-NLS-1$ //$NON-NLS-2$
86  									+ "checkExitCode: " + checkExitCode + "\n" //$NON-NLS-1$ //$NON-NLS-2$
87  									+ "execError: " + execError + "\n" //$NON-NLS-1$ //$NON-NLS-2$
88  									+ "stderr: \n" //$NON-NLS-1$
89  									+ new String(
90  											result.getStderr().toByteArray()),
91  							result, execError);
92  				}
93  			}
94  			return result;
95  		} finally {
96  			deleteCommandArray();
97  		}
98  	}
99  
100 	/**
101 	 * @param path
102 	 *            the executable path
103 	 * @param workingDir
104 	 *            the working directory
105 	 * @param env
106 	 *            the environment
107 	 * @return the execution result
108 	 * @throws ToolException
109 	 * @throws InterruptedException
110 	 * @throws IOException
111 	 */
112 	public boolean checkExecutable(String path, File workingDir,
113 			Map<String, String> env)
114 			throws ToolException, IOException, InterruptedException {
115 		checkUseMsys2(path);
116 		String command = null;
117 		if (fs instanceof FS_Win32 && !useMsys2) {
118 			Path p = Paths.get(path);
119 			// Win32 (and not cygwin or MSYS2) where accepts only command / exe
120 			// name as parameter
121 			// so check if exists and executable in this case
122 			if (p.isAbsolute() && Files.isExecutable(p)) {
123 				return true;
124 			}
125 			// try where command for all other cases
126 			command = "where " + ExternalToolUtils.quotePath(path); //$NON-NLS-1$
127 		} else {
128 			command = "which " + ExternalToolUtils.quotePath(path); //$NON-NLS-1$
129 		}
130 		boolean available = true;
131 		try {
132 			ExecutionResult rc = run(command, workingDir, env);
133 			if (rc.getRc() != 0) {
134 				available = false;
135 			}
136 		} catch (IOException | InterruptedException | NoWorkTreeException
137 				| ToolException e) {
138 			// no op: is true to not hide possible tools from user
139 		}
140 		return available;
141 	}
142 
143 	private void deleteCommandArray() {
144 		deleteCommandFile();
145 	}
146 
147 	private String[] createCommandArray(String command)
148 			throws ToolException, IOException {
149 		String[] commandArray = null;
150 		checkUseMsys2(command);
151 		createCommandFile(command);
152 		if (fs instanceof FS_POSIX) {
153 			commandArray = new String[1];
154 			commandArray[0] = commandFile.getCanonicalPath();
155 		} else if (fs instanceof FS_Win32) {
156 			if (useMsys2) {
157 				commandArray = new String[3];
158 				commandArray[0] = "bash.exe"; //$NON-NLS-1$
159 				commandArray[1] = "-c"; //$NON-NLS-1$
160 				commandArray[2] = commandFile.getCanonicalPath().replace("\\", //$NON-NLS-1$
161 						"/"); //$NON-NLS-1$
162 			} else {
163 				commandArray = new String[1];
164 				commandArray[0] = commandFile.getCanonicalPath();
165 			}
166 		} else if (fs instanceof FS_Win32_Cygwin) {
167 			commandArray = new String[1];
168 			commandArray[0] = commandFile.getCanonicalPath().replace("\\", "/"); //$NON-NLS-1$ //$NON-NLS-2$
169 		} else {
170 			throw new ToolException(
171 					"JGit: file system not supported: " + fs.toString()); //$NON-NLS-1$
172 		}
173 		return commandArray;
174 	}
175 
176 	private void checkUseMsys2(String command) {
177 		useMsys2 = false;
178 		String useMsys2Str = System.getProperty("jgit.usemsys2bash"); //$NON-NLS-1$
179 		if (!StringUtils.isEmptyOrNull(useMsys2Str)) {
180 			if (useMsys2Str.equalsIgnoreCase("auto")) { //$NON-NLS-1$
181 				useMsys2 = command.contains(".sh"); //$NON-NLS-1$
182 			} else {
183 				useMsys2 = Boolean.parseBoolean(useMsys2Str);
184 			}
185 		}
186 	}
187 
188 	private void createCommandFile(String command)
189 			throws ToolException, IOException {
190 		String fileExtension = null;
191 		if (useMsys2 || fs instanceof FS_POSIX
192 				|| fs instanceof FS_Win32_Cygwin) {
193 			fileExtension = ".sh"; //$NON-NLS-1$
194 		} else if (fs instanceof FS_Win32) {
195 			fileExtension = ".cmd"; //$NON-NLS-1$
196 			command = "@echo off" + System.lineSeparator() + command //$NON-NLS-1$
197 					+ System.lineSeparator() + "exit /B %ERRORLEVEL%"; //$NON-NLS-1$
198 		} else {
199 			throw new ToolException(
200 					"JGit: file system not supported: " + fs.toString()); //$NON-NLS-1$
201 		}
202 		commandFile = File.createTempFile(".__", //$NON-NLS-1$
203 				"__jgit_tool" + fileExtension); //$NON-NLS-1$
204 		try (OutputStream outStream = new FileOutputStream(commandFile)) {
205 			byte[] strToBytes = command.getBytes();
206 			outStream.write(strToBytes);
207 			outStream.close();
208 		}
209 		commandFile.setExecutable(true);
210 	}
211 
212 	private void deleteCommandFile() {
213 		if (commandFile != null && commandFile.exists()) {
214 			commandFile.delete();
215 		}
216 	}
217 
218 	private boolean isCommandExecutionError(int rc) {
219 		if (useMsys2 || fs instanceof FS_POSIX
220 				|| fs instanceof FS_Win32_Cygwin) {
221 			// 126: permission for executing command denied
222 			// 127: command not found
223 			if ((rc == 126) || (rc == 127)) {
224 				return true;
225 			}
226 		}
227 		else if (fs instanceof FS_Win32) {
228 			// 9009, 0x2331: Program is not recognized as an internal or
229 			// external command, operable program or batch file. Indicates that
230 			// command, application name or path has been misspelled when
231 			// configuring the Action.
232 			if (rc == 9009) {
233 				return true;
234 			}
235 		}
236 		return false;
237 	}
238 
239 }