Enhance FS.runProcess() to support stdin-redirection and binary data
In order to support filters in gitattributes FS.runProcess() is made public. Support for stdin redirection has been added. Support for binary data on stdin/stdout (as used be clean/smudge filters) has been added. Change-Id: Ice2c152e9391368dc5748d7b825a838e3eb755f9 Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
This commit is contained in:
parent
8473bc4f8d
commit
67a77d402a
|
@ -0,0 +1,171 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2015, Christian Halstrick <christian.halstrick@sap.com>
|
||||||
|
* and other copyright owners as documented in the project's IP log.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available
|
||||||
|
* under the terms of the Eclipse Distribution License v1.0 which
|
||||||
|
* accompanies this distribution, is reproduced below, and is
|
||||||
|
* available at http://www.eclipse.org/org/documents/edl-v10.php
|
||||||
|
*
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or
|
||||||
|
* without modification, are permitted provided that the following
|
||||||
|
* conditions are met:
|
||||||
|
*
|
||||||
|
* - Redistributions of source code must retain the above copyright
|
||||||
|
* notice, this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* - Redistributions in binary form must reproduce the above
|
||||||
|
* copyright notice, this list of conditions and the following
|
||||||
|
* disclaimer in the documentation and/or other materials provided
|
||||||
|
* with the distribution.
|
||||||
|
*
|
||||||
|
* - Neither the name of the Eclipse Foundation, Inc. nor the
|
||||||
|
* names of its contributors may be used to endorse or promote
|
||||||
|
* products derived from this software without specific prior
|
||||||
|
* written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||||
|
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||||
|
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||||
|
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||||
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||||
|
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||||
|
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||||
|
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.eclipse.jgit.util;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.junit.JGitTestUtil;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class RunExternalScriptTest {
|
||||||
|
private ByteArrayOutputStream out;
|
||||||
|
|
||||||
|
private ByteArrayOutputStream err;
|
||||||
|
|
||||||
|
private String sep = System.getProperty("line.separator");
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() throws Exception {
|
||||||
|
out = new ByteArrayOutputStream();
|
||||||
|
err = new ByteArrayOutputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCopyStdIn() throws IOException, InterruptedException {
|
||||||
|
String inputStr = "a\nb\rc\r\nd";
|
||||||
|
File script = writeTempFile("cat -");
|
||||||
|
int rc = FS.DETECTED.runProcess(
|
||||||
|
new ProcessBuilder("/bin/sh", script.getPath()), out, err,
|
||||||
|
new ByteArrayInputStream(inputStr.getBytes()));
|
||||||
|
assertEquals(0, rc);
|
||||||
|
assertEquals(inputStr, new String(out.toByteArray()));
|
||||||
|
assertEquals("", new String(err.toByteArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCopyNullStdIn() throws IOException, InterruptedException {
|
||||||
|
File script = writeTempFile("cat -");
|
||||||
|
int rc = FS.DETECTED.runProcess(
|
||||||
|
new ProcessBuilder("/bin/sh", script.getPath()), out, err,
|
||||||
|
(InputStream) null);
|
||||||
|
assertEquals(0, rc);
|
||||||
|
assertEquals("", new String(out.toByteArray()));
|
||||||
|
assertEquals("", new String(err.toByteArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testArguments() throws IOException, InterruptedException {
|
||||||
|
File script = writeTempFile("echo $#,$1,$2,$3,$4,$5,$6");
|
||||||
|
int rc = FS.DETECTED.runProcess(new ProcessBuilder("/bin/bash",
|
||||||
|
script.getPath(), "a", "b", "c"), out, err, (InputStream) null);
|
||||||
|
assertEquals(0, rc);
|
||||||
|
assertEquals("3,a,b,c,,,\n", new String(out.toByteArray()));
|
||||||
|
assertEquals("", new String(err.toByteArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRc() throws IOException, InterruptedException {
|
||||||
|
File script = writeTempFile("exit 3");
|
||||||
|
int rc = FS.DETECTED.runProcess(
|
||||||
|
new ProcessBuilder("/bin/sh", script.getPath(), "a", "b", "c"),
|
||||||
|
out, err, (InputStream) null);
|
||||||
|
assertEquals(3, rc);
|
||||||
|
assertEquals("", new String(out.toByteArray()));
|
||||||
|
assertEquals("", new String(err.toByteArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNullStdout() throws IOException, InterruptedException {
|
||||||
|
File script = writeTempFile("echo hi");
|
||||||
|
int rc = FS.DETECTED.runProcess(
|
||||||
|
new ProcessBuilder("/bin/sh", script.getPath()), null, err,
|
||||||
|
(InputStream) null);
|
||||||
|
assertEquals(0, rc);
|
||||||
|
assertEquals("", new String(out.toByteArray()));
|
||||||
|
assertEquals("", new String(err.toByteArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStdErr() throws IOException, InterruptedException {
|
||||||
|
File script = writeTempFile("echo hi >&2");
|
||||||
|
int rc = FS.DETECTED.runProcess(
|
||||||
|
new ProcessBuilder("/bin/sh", script.getPath()), null, err,
|
||||||
|
(InputStream) null);
|
||||||
|
assertEquals(0, rc);
|
||||||
|
assertEquals("", new String(out.toByteArray()));
|
||||||
|
assertEquals("hi" + sep, new String(err.toByteArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAllTogetherBin() throws IOException, InterruptedException {
|
||||||
|
String inputStr = "a\nb\rc\r\nd";
|
||||||
|
File script = writeTempFile("echo $#,$1,$2,$3,$4,$5,$6 >&2 ; cat -; exit 5");
|
||||||
|
int rc = FS.DETECTED.runProcess(
|
||||||
|
new ProcessBuilder("/bin/sh", script.getPath(), "a", "b", "c"),
|
||||||
|
out, err, new ByteArrayInputStream(inputStr.getBytes()));
|
||||||
|
assertEquals(5, rc);
|
||||||
|
assertEquals(inputStr, new String(out.toByteArray()));
|
||||||
|
assertEquals("3,a,b,c,,," + sep, new String(err.toByteArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IOException.class)
|
||||||
|
public void testWrongSh() throws IOException, InterruptedException {
|
||||||
|
File script = writeTempFile("cat -");
|
||||||
|
FS.DETECTED.runProcess(
|
||||||
|
new ProcessBuilder("/bin/sh-foo", script.getPath(), "a", "b",
|
||||||
|
"c"), out, err, (InputStream) null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWrongScript() throws IOException, InterruptedException {
|
||||||
|
File script = writeTempFile("cat-foo -");
|
||||||
|
int rc = FS.DETECTED.runProcess(
|
||||||
|
new ProcessBuilder("/bin/sh", script.getPath(), "a", "b", "c"),
|
||||||
|
out, err, (InputStream) null);
|
||||||
|
assertEquals(127, rc);
|
||||||
|
}
|
||||||
|
|
||||||
|
private File writeTempFile(String body) throws IOException {
|
||||||
|
File f = File.createTempFile("RunProcessTestScript_", "");
|
||||||
|
JGitTestUtil.write(f, body);
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,15 +44,13 @@
|
||||||
package org.eclipse.jgit.util;
|
package org.eclipse.jgit.util;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.BufferedWriter;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.OutputStreamWriter;
|
|
||||||
import java.io.PrintStream;
|
import java.io.PrintStream;
|
||||||
import java.io.PrintWriter;
|
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.security.AccessController;
|
import java.security.AccessController;
|
||||||
import java.security.PrivilegedAction;
|
import java.security.PrivilegedAction;
|
||||||
|
@ -864,52 +862,88 @@ public File findHook(Repository repository, final String hookName) {
|
||||||
* Runs the given process until termination, clearing its stdout and stderr
|
* Runs the given process until termination, clearing its stdout and stderr
|
||||||
* streams on-the-fly.
|
* streams on-the-fly.
|
||||||
*
|
*
|
||||||
* @param hookProcessBuilder
|
* @param processBuilder
|
||||||
* The process builder configured for this hook.
|
* The process builder configured for this process.
|
||||||
* @param outRedirect
|
* @param outRedirect
|
||||||
* A print stream on which to redirect the hook's stdout. Can be
|
* A OutputStream on which to redirect the processes stdout. Can
|
||||||
* <code>null</code>, in which case the hook's standard output
|
* be <code>null</code>, in which case the processes standard
|
||||||
* will be lost.
|
* output will be lost.
|
||||||
* @param errRedirect
|
* @param errRedirect
|
||||||
* A print stream on which to redirect the hook's stderr. Can be
|
* A OutputStream on which to redirect the processes stderr. Can
|
||||||
* <code>null</code>, in which case the hook's standard error
|
* be <code>null</code>, in which case the processes standard
|
||||||
* will be lost.
|
* error will be lost.
|
||||||
* @param stdinArgs
|
* @param stdinArgs
|
||||||
* A string to pass on to the standard input of the hook. Can be
|
* A string to pass on to the standard input of the hook. Can be
|
||||||
* <code>null</code>.
|
* <code>null</code>.
|
||||||
* @return the exit value of this hook.
|
* @return the exit value of this process.
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
* if an I/O error occurs while executing this hook.
|
* if an I/O error occurs while executing this process.
|
||||||
* @throws InterruptedException
|
* @throws InterruptedException
|
||||||
* if the current thread is interrupted while waiting for the
|
* if the current thread is interrupted while waiting for the
|
||||||
* process to end.
|
* process to end.
|
||||||
* @since 3.7
|
* @since 4.2
|
||||||
*/
|
*/
|
||||||
protected int runProcess(ProcessBuilder hookProcessBuilder,
|
public int runProcess(ProcessBuilder processBuilder,
|
||||||
OutputStream outRedirect, OutputStream errRedirect, String stdinArgs)
|
OutputStream outRedirect, OutputStream errRedirect, String stdinArgs)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
|
InputStream in = (stdinArgs == null) ? null : new ByteArrayInputStream(
|
||||||
|
stdinArgs.getBytes(Constants.CHARACTER_ENCODING));
|
||||||
|
return runProcess(processBuilder, outRedirect, errRedirect, in);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the given process until termination, clearing its stdout and stderr
|
||||||
|
* streams on-the-fly.
|
||||||
|
*
|
||||||
|
* @param processBuilder
|
||||||
|
* The process builder configured for this process.
|
||||||
|
* @param outRedirect
|
||||||
|
* An OutputStream on which to redirect the processes stdout. Can
|
||||||
|
* be <code>null</code>, in which case the processes standard
|
||||||
|
* output will be lost. If binary is set to <code>false</code>
|
||||||
|
* then it is expected that the process emits text data which
|
||||||
|
* should be processed line by line.
|
||||||
|
* @param errRedirect
|
||||||
|
* An OutputStream on which to redirect the processes stderr. Can
|
||||||
|
* be <code>null</code>, in which case the processes standard
|
||||||
|
* error will be lost.
|
||||||
|
* @param inRedirect
|
||||||
|
* An InputStream from which to redirect the processes stdin. Can
|
||||||
|
* be <code>null</code>, in which case the process doesn't get
|
||||||
|
* any data over stdin. If binary is set to
|
||||||
|
* <code>false</code> then it is expected that the process
|
||||||
|
* expects text data which should be processed line by line.
|
||||||
|
* @return the return code of this process.
|
||||||
|
* @throws IOException
|
||||||
|
* if an I/O error occurs while executing this process.
|
||||||
|
* @throws InterruptedException
|
||||||
|
* if the current thread is interrupted while waiting for the
|
||||||
|
* process to end.
|
||||||
|
* @since 4.2
|
||||||
|
*/
|
||||||
|
public int runProcess(ProcessBuilder processBuilder,
|
||||||
|
OutputStream outRedirect, OutputStream errRedirect,
|
||||||
|
InputStream inRedirect) throws IOException,
|
||||||
|
InterruptedException {
|
||||||
final ExecutorService executor = Executors.newFixedThreadPool(2);
|
final ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||||
Process process = null;
|
Process process = null;
|
||||||
// We'll record the first I/O exception that occurs, but keep on trying
|
// We'll record the first I/O exception that occurs, but keep on trying
|
||||||
// to dispose of our open streams and file handles
|
// to dispose of our open streams and file handles
|
||||||
IOException ioException = null;
|
IOException ioException = null;
|
||||||
try {
|
try {
|
||||||
process = hookProcessBuilder.start();
|
process = processBuilder.start();
|
||||||
final Callable<Void> errorGobbler = new StreamGobbler(
|
final Callable<Void> errorGobbler = new StreamGobbler(
|
||||||
process.getErrorStream(), errRedirect);
|
process.getErrorStream(), errRedirect);
|
||||||
final Callable<Void> outputGobbler = new StreamGobbler(
|
final Callable<Void> outputGobbler = new StreamGobbler(
|
||||||
process.getInputStream(), outRedirect);
|
process.getInputStream(), outRedirect);
|
||||||
executor.submit(errorGobbler);
|
executor.submit(errorGobbler);
|
||||||
executor.submit(outputGobbler);
|
executor.submit(outputGobbler);
|
||||||
if (stdinArgs != null) {
|
OutputStream outputStream = process.getOutputStream();
|
||||||
final PrintWriter stdinWriter = new PrintWriter(
|
if (inRedirect != null) {
|
||||||
process.getOutputStream());
|
new StreamGobbler(inRedirect, outputStream)
|
||||||
stdinWriter.print(stdinArgs);
|
.call();
|
||||||
stdinWriter.flush();
|
|
||||||
// We are done with this hook's input. Explicitly close its
|
|
||||||
// stdin now to kick off any blocking read the hook might have.
|
|
||||||
stdinWriter.close();
|
|
||||||
}
|
}
|
||||||
|
outputStream.close();
|
||||||
return process.waitFor();
|
return process.waitFor();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
ioException = e;
|
ioException = e;
|
||||||
|
@ -1187,30 +1221,27 @@ public String normalize(String name) {
|
||||||
* </p>
|
* </p>
|
||||||
*/
|
*/
|
||||||
private static class StreamGobbler implements Callable<Void> {
|
private static class StreamGobbler implements Callable<Void> {
|
||||||
private final BufferedReader reader;
|
private InputStream in;
|
||||||
|
|
||||||
private final BufferedWriter writer;
|
private OutputStream out;
|
||||||
|
|
||||||
public StreamGobbler(InputStream stream, OutputStream output) {
|
public StreamGobbler(InputStream stream, OutputStream output) {
|
||||||
this.reader = new BufferedReader(new InputStreamReader(stream));
|
this.in = stream;
|
||||||
if (output == null)
|
this.out = output;
|
||||||
this.writer = null;
|
|
||||||
else
|
|
||||||
this.writer = new BufferedWriter(new OutputStreamWriter(output));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Void call() throws IOException {
|
public Void call() throws IOException {
|
||||||
boolean writeFailure = false;
|
boolean writeFailure = false;
|
||||||
|
byte buffer[] = new byte[4096];
|
||||||
String line = null;
|
int readBytes;
|
||||||
while ((line = reader.readLine()) != null) {
|
while ((readBytes = in.read(buffer)) != -1) {
|
||||||
// Do not try to write again after a failure, but keep reading
|
// Do not try to write again after a failure, but keep
|
||||||
// as long as possible to prevent the input stream from choking.
|
// reading as long as possible to prevent the input stream
|
||||||
if (!writeFailure && writer != null) {
|
// from choking.
|
||||||
|
if (!writeFailure && out != null) {
|
||||||
try {
|
try {
|
||||||
writer.write(line);
|
out.write(buffer, 0, readBytes);
|
||||||
writer.newLine();
|
out.flush();
|
||||||
writer.flush();
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
writeFailure = true;
|
writeFailure = true;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue