From: Christian Halstrick Date: Wed, 17 Jun 2015 12:54:11 +0000 (+0200) Subject: Enhance FS.runProcess() to support stdin-redirection and binary data X-Git-Tag: v4.2.0.201511101648-m1~20 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=67a77d402aa4bab609cb639fee9d18f42aed1d59;p=jgit.git 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 --- diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RunExternalScriptTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RunExternalScriptTest.java new file mode 100644 index 0000000000..82beab2dc8 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RunExternalScriptTest.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2015, Christian Halstrick + * 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; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java index 4e4371e8db..bcaf62a0c4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java @@ -44,15 +44,13 @@ package org.eclipse.jgit.util; import java.io.BufferedReader; -import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; -import java.io.OutputStreamWriter; import java.io.PrintStream; -import java.io.PrintWriter; import java.nio.charset.Charset; import java.security.AccessController; import java.security.PrivilegedAction; @@ -864,52 +862,88 @@ public abstract class FS { * Runs the given process until termination, clearing its stdout and stderr * streams on-the-fly. * - * @param hookProcessBuilder - * The process builder configured for this hook. + * @param processBuilder + * The process builder configured for this process. * @param outRedirect - * A print stream on which to redirect the hook's stdout. Can be - * null, in which case the hook's standard output - * will be lost. + * A OutputStream on which to redirect the processes stdout. Can + * be null, in which case the processes standard + * output will be lost. * @param errRedirect - * A print stream on which to redirect the hook's stderr. Can be - * null, in which case the hook's standard error - * will be lost. + * A OutputStream on which to redirect the processes stderr. Can + * be null, in which case the processes standard + * error will be lost. * @param stdinArgs * A string to pass on to the standard input of the hook. Can be * null. - * @return the exit value of this hook. + * @return the exit value of this process. * @throws IOException - * if an I/O error occurs while executing this hook. + * 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 3.7 + * @since 4.2 */ - protected int runProcess(ProcessBuilder hookProcessBuilder, + public int runProcess(ProcessBuilder processBuilder, OutputStream outRedirect, OutputStream errRedirect, String stdinArgs) 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 null, in which case the processes standard + * output will be lost. If binary is set to false + * 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 null, in which case the processes standard + * error will be lost. + * @param inRedirect + * An InputStream from which to redirect the processes stdin. Can + * be null, in which case the process doesn't get + * any data over stdin. If binary is set to + * false 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); Process process = null; // We'll record the first I/O exception that occurs, but keep on trying // to dispose of our open streams and file handles IOException ioException = null; try { - process = hookProcessBuilder.start(); + process = processBuilder.start(); final Callable errorGobbler = new StreamGobbler( process.getErrorStream(), errRedirect); final Callable outputGobbler = new StreamGobbler( process.getInputStream(), outRedirect); executor.submit(errorGobbler); executor.submit(outputGobbler); - if (stdinArgs != null) { - final PrintWriter stdinWriter = new PrintWriter( - process.getOutputStream()); - stdinWriter.print(stdinArgs); - 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 outputStream = process.getOutputStream(); + if (inRedirect != null) { + new StreamGobbler(inRedirect, outputStream) + .call(); } + outputStream.close(); return process.waitFor(); } catch (IOException e) { ioException = e; @@ -1187,30 +1221,27 @@ public abstract class FS { *

*/ private static class StreamGobbler implements Callable { - private final BufferedReader reader; + private InputStream in; - private final BufferedWriter writer; + private OutputStream out; public StreamGobbler(InputStream stream, OutputStream output) { - this.reader = new BufferedReader(new InputStreamReader(stream)); - if (output == null) - this.writer = null; - else - this.writer = new BufferedWriter(new OutputStreamWriter(output)); + this.in = stream; + this.out = output; } public Void call() throws IOException { boolean writeFailure = false; - - String line = null; - while ((line = reader.readLine()) != null) { - // Do not try to write again after a failure, but keep reading - // as long as possible to prevent the input stream from choking. - if (!writeFailure && writer != null) { + byte buffer[] = new byte[4096]; + int readBytes; + while ((readBytes = in.read(buffer)) != -1) { + // Do not try to write again after a failure, but keep + // reading as long as possible to prevent the input stream + // from choking. + if (!writeFailure && out != null) { try { - writer.write(line); - writer.newLine(); - writer.flush(); + out.write(buffer, 0, readBytes); + out.flush(); } catch (IOException e) { writeFailure = true; }