| package org.apache.helix; |
| |
| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import java.io.BufferedInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.UnsupportedEncodingException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.TimeoutException; |
| |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * Wrapper for running commands outside of the JVM |
| * @see {@link Process} |
| */ |
| public class ExternalCommand { |
| public static final String MODULE = ExternalCommand.class.getName(); |
| public static final Logger LOG = LoggerFactory.getLogger(MODULE); |
| |
| private final ProcessBuilder _processBuilder; |
| |
| private Process _process; |
| private InputReader _out; |
| private InputReader _err; |
| |
| /** |
| * Stream redirector |
| */ |
| private static class InputReader extends Thread { |
| private static final int BUFFER_SIZE = 2048; |
| |
| private final InputStream _in; |
| private final ByteArrayOutputStream _out; |
| private boolean _running = false; |
| |
| InputReader(InputStream in) { |
| _in = in; |
| _out = new ByteArrayOutputStream(); |
| } |
| |
| @Override |
| public void run() { |
| _running = true; |
| |
| byte[] buf = new byte[BUFFER_SIZE]; |
| int n = 0; |
| try { |
| while ((n = _in.read(buf)) != -1) |
| _out.write(buf, 0, n); |
| } catch (IOException e) { |
| LOG.error("error while reading external command", e); |
| } |
| |
| _running = false; |
| } |
| |
| public byte[] getOutput() { |
| if (_running) |
| throw new IllegalStateException("wait for process to be completed"); |
| |
| return _out.toByteArray(); |
| } |
| } |
| |
| /** |
| * Initialize with a {@link ProcessBuilder} |
| * @param processBuilder initialized {@link ProcessBuilder} object |
| */ |
| public ExternalCommand(ProcessBuilder processBuilder) { |
| _processBuilder = processBuilder; |
| } |
| |
| /** |
| * After creating the command, you have to start it... |
| * @throws IOException |
| */ |
| public void start() throws IOException { |
| _process = _processBuilder.start(); |
| _out = new InputReader(new BufferedInputStream(_process.getInputStream())); |
| _err = new InputReader(new BufferedInputStream(_process.getErrorStream())); |
| |
| _out.start(); |
| _err.start(); |
| } |
| |
| /** |
| * @see {@link ProcessBuilder#environment()} |
| */ |
| public Map<String, String> getEnvironment() { |
| return _processBuilder.environment(); |
| } |
| |
| /** |
| * @see {@link ProcessBuilder#directory()} |
| */ |
| public File getWorkingDirectory() { |
| return _processBuilder.directory(); |
| } |
| |
| /** |
| * @see {@link ProcessBuilder#directory(File)} |
| */ |
| public void setWorkingDirectory(File directory) { |
| _processBuilder.directory(directory); |
| } |
| |
| /** |
| * @see {@link ProcessBuilder#redirectErrorStream()} |
| */ |
| public boolean getRedirectErrorStream() { |
| return _processBuilder.redirectErrorStream(); |
| } |
| |
| /** |
| * @see {@link ProcessBuilder#redirectErrorStream(boolean)} |
| */ |
| public void setRedirectErrorStream(boolean redirectErrorStream) { |
| _processBuilder.redirectErrorStream(redirectErrorStream); |
| } |
| |
| /** |
| * Get the contents of the output stream after completion |
| * @return bytes from the output stream |
| * @throws InterruptedException the process was interrupted before completion |
| */ |
| public byte[] getOutput() throws InterruptedException { |
| waitFor(); |
| return _out.getOutput(); |
| } |
| |
| /** |
| * Get the contents of the error stream after completion |
| * @return bytes from the error stream |
| * @throws InterruptedException the process was interrupted before completion |
| */ |
| public byte[] getError() throws InterruptedException { |
| waitFor(); |
| return _err.getOutput(); |
| } |
| |
| /** |
| * Returns the output as a string. |
| * @param encoding string encoding scheme, e.g. "UTF-8" |
| * @return encoded string |
| * @throws InterruptedException the process was interrupted before completion |
| * @throws UnsupportedEncodingException the encoding scheme is invalid |
| */ |
| public String getStringOutput(String encoding) throws InterruptedException, |
| UnsupportedEncodingException { |
| return new String(getOutput(), encoding); |
| } |
| |
| /** |
| * Returns the output as a string. Uses encoding "UTF-8". |
| * @return utf8 encoded string |
| * @throws InterruptedException the process was interrupted before completion |
| */ |
| public String getStringOutput() throws InterruptedException { |
| try { |
| return getStringOutput("UTF-8"); |
| } catch (UnsupportedEncodingException e) { |
| // should not happen |
| throw new RuntimeException(e); |
| } |
| } |
| |
| /** |
| * Returns the error as a string. |
| * @param encoding the encoding scheme, e.g. "UTF-8" |
| * @return error as string |
| * @throws InterruptedException the process was interrupted before completion |
| * @throws UnsupportedEncodingException the encoding scheme is invalid |
| */ |
| public String getStringError(String encoding) throws InterruptedException, |
| UnsupportedEncodingException { |
| return new String(getError(), encoding); |
| } |
| |
| /** |
| * Returns the error as a string. Uses encoding "UTF-8". |
| * @return error as string |
| * @throws InterruptedException the process was interrupted before completion |
| */ |
| public String getStringError() throws InterruptedException { |
| try { |
| return getStringError("UTF-8"); |
| } catch (UnsupportedEncodingException e) { |
| // should not happen |
| throw new RuntimeException(e); |
| } |
| } |
| |
| /** |
| * Properly waits until everything is complete: joins on the thread that |
| * reads the output, joins on the thread that reads the error and finally |
| * wait for the process to be finished. |
| * @return the status code of the process. |
| * @throws InterruptedException the process was interrupted before completion |
| */ |
| public int waitFor() throws InterruptedException { |
| if (_process == null) |
| throw new IllegalStateException("you must call start first"); |
| |
| _out.join(); |
| _err.join(); |
| return _process.waitFor(); |
| } |
| |
| /** |
| * Properly waits until everything is complete: joins on the thread that |
| * reads the output, joins on the thread that reads the error and finally |
| * wait for the process to be finished. |
| * If the process has not completed before the timeout, throws a {@link TimeoutException} |
| * @return the status code of the process. |
| * @throws TimeoutException the process timed out |
| * @throws InterruptedException the process was interrupted before completion |
| */ |
| public int waitFor(long timeout) throws InterruptedException, TimeoutException { |
| if (_process == null) |
| throw new IllegalStateException("you must call start first"); |
| |
| // Chronos c = new Chronos(); |
| _out.join(timeout); |
| // timeout -= c.tick(); |
| if (timeout <= 0) |
| throw new TimeoutException("Wait timed out"); |
| _err.join(timeout); |
| // timeout -= c.tick(); |
| if (timeout <= 0) |
| throw new TimeoutException("Wait timed out"); |
| |
| // there is no timeout in this API, not much we can do here |
| // waiting on the other two threads should give us some safety |
| return _process.waitFor(); |
| } |
| |
| /** |
| * @see {@link Process#exitValue()} |
| * @return the return code of the process |
| */ |
| public int exitValue() { |
| if (_process == null) |
| throw new IllegalStateException("you must call start first"); |
| |
| return _process.exitValue(); |
| } |
| |
| /** |
| * see {@link Process#destroy()} |
| */ |
| public void destroy() { |
| if (_process == null) |
| throw new IllegalStateException("you must call start first"); |
| |
| _process.destroy(); |
| } |
| |
| /** |
| * Creates an external process from the command. It is not started and you have to call |
| * start on it! |
| * @param commands the command to execute |
| * @return the process |
| */ |
| public static ExternalCommand create(String... commands) { |
| ExternalCommand ec = new ExternalCommand(new ProcessBuilder(commands)); |
| return ec; |
| } |
| |
| /** |
| * Creates an external process from the command. It is not started and you have to call |
| * start on it! |
| * @param commands the command to execute |
| * @return the process |
| */ |
| public static ExternalCommand create(List<String> commands) { |
| ExternalCommand ec = new ExternalCommand(new ProcessBuilder(commands)); |
| return ec; |
| } |
| |
| /** |
| * Creates an external process from the command. The command is executed. |
| * @param commands the commands to execute |
| * @return the process |
| * @throws IOException if there is an error |
| */ |
| public static ExternalCommand start(String... commands) throws IOException { |
| ExternalCommand ec = new ExternalCommand(new ProcessBuilder(commands)); |
| ec.start(); |
| return ec; |
| } |
| |
| /** |
| * Executes the external command in the given working directory and waits for it to be |
| * finished. |
| * @param workingDirectory the root directory from where to run the command |
| * @param command the command to execute (should be relative to the working directory |
| * @param args the arguments to the command |
| * @return the process |
| */ |
| public static ExternalCommand execute(File workingDirectory, String command, String... args) |
| throws IOException, InterruptedException { |
| try { |
| return executeWithTimeout(workingDirectory, command, 0, args); |
| } catch (TimeoutException e) { |
| // Can't happen! |
| throw new IllegalStateException(MODULE + ".execute: Unexpected timeout occurred!"); |
| } |
| } |
| |
| /** |
| * Executes the external command in the given working directory and waits (until timeout |
| * is elapsed) for it to be finished. |
| * @param workingDirectory |
| * the root directory from where to run the command |
| * @param command |
| * the command to execute (should be relative to the working directory |
| * @param timeout |
| * the maximum amount of time to wait for this external command (in ms). If |
| * this value is less than or equal to 0, timeout is ignored |
| * @param args |
| * the arguments to the command |
| * @return the process |
| */ |
| public static ExternalCommand executeWithTimeout(File workingDirectory, String command, |
| long timeout, String... args) throws IOException, InterruptedException, TimeoutException { |
| List<String> arguments = new ArrayList<String>(args.length + 1); |
| |
| arguments.add(new File(workingDirectory, command).getAbsolutePath()); |
| arguments.addAll(Arrays.asList(args)); |
| |
| ExternalCommand cmd = ExternalCommand.create(arguments); |
| |
| cmd.setWorkingDirectory(workingDirectory); |
| |
| cmd.setRedirectErrorStream(true); |
| |
| cmd.start(); |
| |
| /* Use timeout if it is a valid value! */ |
| if (timeout <= 0) |
| cmd.waitFor(); |
| else |
| cmd.waitFor(timeout); |
| |
| if (LOG.isDebugEnabled()) |
| LOG.debug(cmd.getStringOutput()); |
| |
| return cmd; |
| } |
| } |