/** * Copyright 2015 Palantir Technologies, Inc. * * Licensed 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. */ package com.palantir.giraffe.command; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.palantir.giraffe.SystemPreconditions.checkSameHost; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.Arrays; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import com.google.common.util.concurrent.Uninterruptibles; import com.palantir.giraffe.command.spi.ExecutionSystemProvider; /** * Static utility methods to create and execute commands. * <p> * In most cases, the methods defined here delegate to the associated execution * system provider to perform the operations. * * @author bkeyes */ public final class Commands { private static final int COPY_BUFFER_SIZE = 2048; /** * Gets a command for the default execution system. * * @param command the command name * @param args the optional arguments */ public static Command get(String command, Object... args) { checkNotNull(command); checkNotNull(args); return getBuilder(command).addArguments(Arrays.asList(args)).build(); } /** * Gets a command for the default execution system. * * @param executable the path to the executable * @param args the optional arguments * * @throws IllegalArgumentException if {@code executable} is not associated * with the default file system */ public static Command get(Path executable, Object... args) { checkNotNull(executable); checkNotNull(args); return getBuilder(executable).addArguments(Arrays.asList(args)).build(); } /** * Gets a command builder for the default execution system. * * @param executable the path to the executable * * @throws IllegalArgumentException if {@code executable} is not associated * with the default file system */ public static Command.Builder getBuilder(Path executable) { checkNotNull(executable); checkSameHost(executable, ExecutionSystems.getDefault()); return getBuilder(executable.toString()); } /** * Gets a command builder for the default execution system. * * @param command the command name */ public static Command.Builder getBuilder(String command) { checkNotNull(command); return ExecutionSystems.getDefault().getCommandBuilder(command); } /** * Synchronously executes a command with the default context. * * @param command the command to execute * * @return the {@linkplain CommandResult result} of executing the command * * @throws CommandException if the command exits with non-zero status * @throws IOException if an I/O error occurs while executing the command * * @see #execute(Command, CommandContext) */ public static CommandResult execute(Command command) throws IOException { return execute(command, CommandContext.defaultContext()); } /** * Synchronously executes a command with the specified context. * <p> * This method blocks until the command terminates. * * @param command the command to execute * @param context the {@link CommandContext} * * @return the {@linkplain CommandResult result} of executing the command * * @throws CommandException if the command exits with a status other than * that specified by the {@link CommandContext} * @throws IOException if an I/O error occurs while executing the command */ public static CommandResult execute(Command command, CommandContext context) throws IOException { checkNotNull(command); checkNotNull(context); return waitFor(executeAsync(command, context)); } /** * Synchronously executes a command with the default context and the given * timeout. * * @param command the command to execute * @param timeout the maximum time to wait for the command to terminate * @param timeUnit the unit of the timeout argument * * @return the {@linkplain CommandResult result} of executing the command * * @throws CommandException if the command exits with a non-zero status * @throws IOException if an I/O error occurs while executing the command * @throws CommandTimeoutException if the timeout is reached before the * command terminates * * @see #execute(Command, CommandContext, long, TimeUnit) */ public static CommandResult execute(Command command, long timeout, TimeUnit timeUnit) throws IOException, CommandTimeoutException { return execute(command, CommandContext.defaultContext(), timeout, timeUnit); } /** * Synchronously executes a command with the given context and timeout. * <p> * This method blocks until the command terminates or the timeout is * reached. When the timeout is reached, a best-effort attempt is made to * cancel the command before throwing a {@code TimeoutException}. * * @param command the command to execute * @param context the {@link CommandContext} * @param timeout the maximum time to wait for the command to terminate * @param unit the unit of the timeout argument * * @return the {@linkplain CommandResult result} of executing the command * * @throws CommandException if the command exits with a status other than * that specified by the {@link CommandContext} * @throws IOException if an I/O error occurs while executing the command * @throws CommandTimeoutException if the timeout is reached before the * command terminates */ public static CommandResult execute(Command command, CommandContext context, long timeout, TimeUnit unit) throws IOException, CommandTimeoutException { checkNotNull(command, "command must be non-null"); checkNotNull(context, "context must be non-null"); checkArgument(timeout >= 0, "timeout must be non-negative."); checkNotNull(unit, "unit must be non-null"); CommandFuture future = executeAsync(command, context); try { return waitFor(future, timeout, unit); } catch (TimeoutException e) { TerminatedCommand failed = new TerminatedCommand(command, context, toResult(future)); throw new CommandTimeoutException(failed, timeout, unit); } } /** * Executes a command asynchronously with the default context. * * @param command the command to execute * * @return a {@link CommandFuture} for the executing command * * @see #executeAsync(Command, CommandContext) */ public static CommandFuture executeAsync(Command command) { return executeAsync(command, CommandContext.defaultContext()); } /** * Executes a command asynchronously with the specified context. Returns a * {@link CommandFuture} that allows clients to wait for the command to * finish and access input and output streams while the command is in * progress. * <p> * Any errors encountered during execution are reported through the returned * future. This includes {@link CommandException}s caused by the * context's exit status check. * <p> * If a command's execution system is closed while it is executing, a * best-effort attempt is made to terminate the command. If this occurs, the * future reports that the command was cancelled. Clients may also cancel * the command explicitly using {@link CommandFuture#cancel(boolean) * CommandFuture.cancel(true)}. * * @param command the command to execute * @param context the {@link CommandContext} * * @return a {@link CommandFuture} for the executing command */ public static CommandFuture executeAsync(Command command, CommandContext context) { checkNotNull(command); checkNotNull(context); return provider(command).execute(command, context); } /** * Waits for the command associated with a {@code CommandFuture} to * terminate. * <p> * This method is uninterruptible; to allow interruption, use * {@link CommandFuture#get()}. * * @param future the {@link CommandFuture} to block on * * @return the {@linkplain CommandResult result} of executing the command * * @throws CommandException if the command fails by throwing a * {@code CommandException} * @throws IOException if an I/O error occurs while executing the command or * the command fails for any other reason. */ public static CommandResult waitFor(CommandFuture future) throws IOException { checkNotNull(future); try { return Uninterruptibles.getUninterruptibly(future); } catch (ExecutionException e) { throw propagateCause(e); } } /** * Waits for the command associated with a {@code CommandFuture} to * terminate or the timeout to be reached. If the timeout is reached, a * best-effort attempt is made to cancel the command before throwing a * {@code TimeoutException}. * <p> * This method is uninterruptible; to allow interruption, use * {@link CommandFuture#get(long, TimeUnit)}. * * @param future the {@link CommandFuture} to block on * * @return the {@linkplain CommandResult result} of executing the command * * @throws TimeoutException if the timeout is reached before the command * terminates * @throws CommandException if the command fails by throwing a * {@code CommandException} * @throws IOException if an I/O error occurs while executing the command or * the command fails for any other reason. */ public static CommandResult waitFor(CommandFuture future, long timeout, TimeUnit unit) throws IOException, TimeoutException { checkNotNull(future); try { return Uninterruptibles.getUninterruptibly(future, timeout, unit); } catch (TimeoutException e) { future.cancel(true); throw new TimeoutException("timeout waiting for command"); } catch (ExecutionException e) { throw propagateCause(e); } } /** * Creates a {@link CommandResult} from the given {@code CommandFuture}. * <p> * If the future is resolved, this method returns the resolved * {@code CommandResult}. Otherwise, a new {@code CommandFuture} is created * using the {@linkplain CommandResult#NO_EXIT_STATUS default exit status} * and any available output. This method never blocks. * * @param future the {@link CommandFuture} to create a result from * * @return a {@code CommandResult} * * @throws IOException if an error occurs while reading output */ public static CommandResult toResult(CommandFuture future) throws IOException { return toResult(future, CommandResult.NO_EXIT_STATUS); } /** * Creates a {@link CommandResult} from the given {@code CommandFuture}. * <p> * If the future is resolved, this method returns the resolved * {@code CommandResult}. Otherwise, a new {@code CommandFuture} is created * using the given exit status and any available output. This method never * blocks. * * @param future the {@link CommandFuture} to create a result from * @param exitStatus the exit status to use if the future is unresolved * * @return a {@code CommandResult} * * @throws IOException if an error occurs while reading output */ public static CommandResult toResult(CommandFuture future, int exitStatus) throws IOException { try { return future.get(0, TimeUnit.MILLISECONDS); } catch (InterruptedException | ExecutionException | TimeoutException | CancellationException ignored) { // ignore, create result using the streams } String stdOut = readAvailable(future.getStdOut(), StandardCharsets.UTF_8); String stdErr = readAvailable(future.getStdErr(), StandardCharsets.UTF_8); return new CommandResult(exitStatus, stdOut, stdErr); } /** * Determines if the given command is associated with the default (local) execution * system. */ public static boolean isLocal(Command command) { checkNotNull(command, "command must be non-null"); return command.getExecutionSystem().equals(ExecutionSystems.getDefault()); } private static String readAvailable(InputStream is, Charset cs) throws IOException { CharBuffer buffer = CharBuffer.allocate(COPY_BUFFER_SIZE); StringBuilder data = new StringBuilder(); InputStreamReader isr = new InputStreamReader(is, cs); while (isr.ready()) { int r = isr.read(buffer); if (r == -1) { break; } buffer.flip(); data.append(buffer); } return data.toString(); } private static IOException propagateCause(ExecutionException e) throws IOException { Throwable cause = e.getCause(); if (cause instanceof CommandException) { CommandException ce = (CommandException) cause; // the stack trace should contain the methods that tried to // execute the command instead of the internal methods that // actually perform the execution ce.fillInStackTrace(); throw ce; } // TODO(bkeyes): use a special subclass of IOException? throw new IOException("execution failed", cause); } private static ExecutionSystemProvider provider(Command c) { checkNotNull(c); return c.getExecutionSystem().provider(); } private Commands() { throw new UnsupportedOperationException(); } }