/* * JBoss, Home of Professional Open Source. * Copyright 2016, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.cli.impl; import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.util.Collection; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.jboss.as.cli.CliConfig; import org.jboss.as.cli.CliEventListener; import org.jboss.as.cli.CommandContext; import org.jboss.as.cli.CommandFormatException; import org.jboss.as.cli.CommandHandler; import org.jboss.as.cli.CommandHistory; import org.jboss.as.cli.CommandLineCompleter; import org.jboss.as.cli.CommandLineException; import org.jboss.as.cli.CommandLineRedirection; import org.jboss.as.cli.ConnectionInfo; import org.jboss.as.cli.ControllerAddress; import org.jboss.as.cli.Util; import org.jboss.as.cli.batch.BatchManager; import org.jboss.as.cli.batch.BatchedCommand; import org.jboss.as.cli.operation.CommandLineParser; import org.jboss.as.cli.operation.NodePathFormatter; import org.jboss.as.cli.operation.OperationCandidatesProvider; import org.jboss.as.cli.operation.OperationRequestAddress; import org.jboss.as.cli.operation.ParsedCommandLine; import org.jboss.as.controller.client.ModelControllerClient; import org.jboss.as.controller.client.Operation; import org.jboss.as.controller.client.OperationMessageHandler; import org.jboss.as.controller.client.OperationResponse; import org.jboss.dmr.ModelNode; import org.jboss.threads.AsyncFuture; /** * Implement the timeout logic when executing commands. Public for testing * purpose. * * @author jdenise@redhat.com */ public class CommandExecutor { // A wrapper to allow to override ModelControllerClient. // Public for testing purpose. public class TimeoutCommandContext implements CommandContext { // A ModelControllerClient that tracks the latest executed Task. // Any attempt to make a request will fail if the command has timeouted. private class TimeoutModelControllerClient implements ModelControllerClient, AwaiterModelControllerClient { private final ModelControllerClient wrapped; TimeoutModelControllerClient(ModelControllerClient wrapped) { this.wrapped = wrapped; } @Override public ModelNode execute(ModelNode operation, OperationMessageHandler messageHandler) throws IOException { return doExecute(operation, messageHandler); } @Override public ModelNode execute(Operation operation, OperationMessageHandler messageHandler) throws IOException { return doExecute(operation, messageHandler); } @Override public ModelNode execute(ModelNode operation) throws IOException { return doExecute(operation); } @Override public ModelNode execute(Operation operation) throws IOException { return doExecute(operation); } @Override public AsyncFuture<ModelNode> executeAsync(ModelNode operation, OperationMessageHandler messageHandler) { return doExecuteAsync(operation, messageHandler); } @Override public AsyncFuture<ModelNode> executeAsync(Operation operation, OperationMessageHandler messageHandler) { return doExecuteAsync(operation, messageHandler); } @Override public OperationResponse executeOperation(Operation operation, OperationMessageHandler messageHandler) throws IOException { return doExecuteOperation(operation, messageHandler); } @Override public AsyncFuture<OperationResponse> executeOperationAsync(Operation operation, OperationMessageHandler messageHandler) { return doExecuteOperationAsync(operation, messageHandler); } @Override public ModelNode execute(ModelNode operation, boolean awaitClose) throws IOException { if (!(wrapped instanceof AwaiterModelControllerClient)) { throw new IOException("Unsupported ModelControllerClient implementation " + wrapped.getClass().getName()); } ModelNode response = execute(operation); if (!Util.isSuccess(response)) { return response; } ((AwaiterModelControllerClient) wrapped).awaitClose(awaitClose); return response; } @Override public void awaitClose(boolean awaitClose) throws IOException { if (!(wrapped instanceof AwaiterModelControllerClient)) { throw new IOException("Unsupported ModelControllerClient implementation " + wrapped.getClass().getName()); } ((AwaiterModelControllerClient) wrapped).awaitClose(awaitClose); } @Override public boolean isConnected() { if (!(wrapped instanceof AwaiterModelControllerClient)) { throw new RuntimeException("Unsupported ModelControllerClient implementation " + wrapped.getClass().getName()); } return ((AwaiterModelControllerClient) wrapped).isConnected(); } @Override public void ensureConnected(long timeoutMillis) throws CommandLineException { if (!(wrapped instanceof AwaiterModelControllerClient)) { throw new CommandLineException("Unsupported ModelControllerClient implementation " + wrapped.getClass().getName()); } ((AwaiterModelControllerClient) wrapped).ensureConnected(timeoutMillis); } private AsyncFuture<OperationResponse> doExecuteOperationAsync(Operation operation, OperationMessageHandler messageHandler) { if (ctx.getCommandTimeout() > 0) { AsyncFuture<OperationResponse> task = wrapped.executeOperationAsync(operation, messageHandler); setLastHandlerTask(task); return task; } else { return wrapped.executeOperationAsync(operation, messageHandler); } } private OperationResponse doExecuteOperation(Operation operation, OperationMessageHandler messageHandler) throws IOException { if (ctx.getCommandTimeout() > 0) { AsyncFuture<OperationResponse> task; task = wrapped.executeOperationAsync(operation, messageHandler); setLastHandlerTask(task); try { return task.get(); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); throw new IOException(ex); } catch (ExecutionException ex) { throw new IOException(ex); } } else { return wrapped.executeOperation(operation, messageHandler); } } private AsyncFuture<ModelNode> doExecuteAsync(Operation operation, OperationMessageHandler messageHandler) { if (ctx.getCommandTimeout() > 0) { AsyncFuture<ModelNode> task = wrapped.executeAsync(operation, messageHandler); setLastHandlerTask(task); return task; } else { return wrapped.executeAsync(operation, messageHandler); } } private AsyncFuture<ModelNode> doExecuteAsync(ModelNode operation, OperationMessageHandler messageHandler) { if (ctx.getCommandTimeout() > 0) { AsyncFuture<ModelNode> task = wrapped.executeAsync(operation, messageHandler); setLastHandlerTask(task); return task; } else { return wrapped.executeAsync(operation, messageHandler); } } private ModelNode doExecute(Operation operation) throws IOException { if (ctx.getCommandTimeout() > 0) { try { Future<ModelNode> task = wrapped.executeAsync(operation, OperationMessageHandler.DISCARD); setLastHandlerTask(task); return task.get(); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); throw new IOException(ex); } catch (ExecutionException ex) { throw new IOException(ex); } catch (Exception ex) { throw new IOException(ex); } } else { return wrapped.execute(operation); } } private ModelNode doExecute(ModelNode operation) throws IOException { if (ctx.getCommandTimeout() > 0) { try { Future<ModelNode> task = wrapped.executeAsync(operation, OperationMessageHandler.DISCARD); setLastHandlerTask(task); return task.get(); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); throw new IOException(ex); } catch (ExecutionException ex) { throw new IOException(ex); } catch (Exception ex) { throw new IOException(ex); } } else { return wrapped.execute(operation); } } private ModelNode doExecute(ModelNode operation, OperationMessageHandler handler) throws IOException { if (ctx.getCommandTimeout() > 0) { try { Future<ModelNode> task = wrapped.executeAsync(operation, handler); setLastHandlerTask(task); return task.get(); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); throw new IOException(ex); } catch (ExecutionException ex) { throw new IOException(ex); } catch (Exception ex) { throw new IOException(ex); } } else { return wrapped.execute(operation, handler); } } private ModelNode doExecute(Operation operation, OperationMessageHandler handler) throws IOException { if (ctx.getCommandTimeout() > 0) { try { Future<ModelNode> task = wrapped.executeAsync(operation, handler); setLastHandlerTask(task); return task.get(); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); throw new IOException(ex); } catch (ExecutionException ex) { throw new IOException(ex); } catch (Exception ex) { throw new IOException(ex); } } else { return wrapped.execute(operation, handler); } } @Override public void close() throws IOException { wrapped.close(); } } private final CommandContext wrapped; private final ModelControllerClient client; private Future<?> handlerTask; private boolean timeout; TimeoutCommandContext(CommandContext wrapped) { this.wrapped = wrapped; this.client = wrapped.getModelControllerClient() == null ? null : new TimeoutModelControllerClient(wrapped.getModelControllerClient()); } synchronized void timeout() { timeout = true; cancelTask(handlerTask); } /** * When an handler execution occurs, multiple calls can be operated. A * reference to the last one (in progress) is stored in order to cancel * it if a timeout occurs. Public for testing purpose. * * @param handlerTask The task to cancel if a timeout occurs. */ public synchronized void setLastHandlerTask(Future<?> handlerTask) { if (timeout) { cancelTask(handlerTask); } else { this.handlerTask = handlerTask; } } // For testing purpose. public Future<?> getLastTask() { return handlerTask; } @Override public CliConfig getConfig() { return wrapped.getConfig(); } @Override public String getArgumentsString() { return wrapped.getArgumentsString(); } @Override public ParsedCommandLine getParsedCommandLine() { return wrapped.getParsedCommandLine(); } @Override public void printLine(String message) { wrapped.printLine(message); } @Override public void printColumns(Collection<String> col) { wrapped.printColumns(col); } @Override public void clearScreen() { wrapped.clearScreen(); } @Override public void terminateSession() { wrapped.terminateSession(); } @Override public boolean isTerminated() { return wrapped.isTerminated(); } @Override public void set(Scope scope, String key, Object value) { wrapped.set(scope, key, value); } @Override public Object get(Scope scope, String key) { return wrapped.get(scope, key); } @Override public void clear(Scope scope) { wrapped.clear(scope); } @Override public Object remove(Scope scope, String key) { return wrapped.remove(scope, key); } @Override public ModelControllerClient getModelControllerClient() { return client; } @Override public void connectController() throws CommandLineException { wrapped.connectController(); } @Override public void connectController(String controller) throws CommandLineException { wrapped.connectController(controller); } @Override public void connectController(String host, int port) throws CommandLineException { wrapped.connectController(host, port); } @Override public void bindClient(ModelControllerClient newClient) { wrapped.bindClient(newClient); } @Override public void disconnectController() { wrapped.disconnectController(); } @Override public String getDefaultControllerHost() { return wrapped.getDefaultControllerHost(); } @Override public int getDefaultControllerPort() { return wrapped.getDefaultControllerPort(); } @Override public ControllerAddress getDefaultControllerAddress() { return wrapped.getDefaultControllerAddress(); } @Override public String getControllerHost() { return wrapped.getControllerHost(); } @Override public int getControllerPort() { return wrapped.getControllerPort(); } @Override public CommandLineParser getCommandLineParser() { return wrapped.getCommandLineParser(); } @Override public OperationRequestAddress getCurrentNodePath() { return wrapped.getCurrentNodePath(); } @Override public NodePathFormatter getNodePathFormatter() { return wrapped.getNodePathFormatter(); } @Override public OperationCandidatesProvider getOperationCandidatesProvider() { return wrapped.getOperationCandidatesProvider(); } @Override public CommandHistory getHistory() { return wrapped.getHistory(); } @Override public boolean isBatchMode() { return wrapped.isBatchMode(); } @Override public boolean isWorkflowMode() { return wrapped.isWorkflowMode(); } @Override public BatchManager getBatchManager() { return wrapped.getBatchManager(); } @Override public BatchedCommand toBatchedCommand(String line) throws CommandFormatException { return wrapped.toBatchedCommand(line); } @Override public ModelNode buildRequest(String line) throws CommandFormatException { return wrapped.buildRequest(line); } @Override public CommandLineCompleter getDefaultCommandCompleter() { return wrapped.getDefaultCommandCompleter(); } @Override public boolean isDomainMode() { return wrapped.isDomainMode(); } @Override public void addEventListener(CliEventListener listener) { wrapped.addEventListener(listener); } @Override public int getExitCode() { return wrapped.getExitCode(); } @Override public void handle(String line) throws CommandLineException { wrapped.handle(line); } @Override public void handleSafe(String line) { wrapped.handleSafe(line); } @Override public void interact() { wrapped.interact(); } @Override public File getCurrentDir() { return wrapped.getCurrentDir(); } @Override public void setCurrentDir(File dir) { wrapped.setCurrentDir(dir); } @Override public boolean isResolveParameterValues() { return wrapped.isResolveParameterValues(); } @Override public void setResolveParameterValues(boolean resolve) { wrapped.setResolveParameterValues(resolve); } @Override public boolean isSilent() { return wrapped.isSilent(); } @Override public void setSilent(boolean silent) { wrapped.setSilent(silent); } @Override public int getTerminalWidth() { return wrapped.getTerminalWidth(); } @Override public int getTerminalHeight() { return wrapped.getTerminalHeight(); } @Override public void setVariable(String name, String value) throws CommandLineException { wrapped.setVariable(name, value); } @Override public String getVariable(String name) { return wrapped.getVariable(name); } @Override public Collection<String> getVariables() { return wrapped.getVariables(); } @Override public void registerRedirection(CommandLineRedirection redirection) throws CommandLineException { wrapped.registerRedirection(redirection); } @Override public ConnectionInfo getConnectionInfo() { return wrapped.getConnectionInfo(); } @Override public void captureOutput(PrintStream captor) { wrapped.captureOutput(captor); } @Override public void releaseOutput() { wrapped.releaseOutput(); } @Override public void setCommandTimeout(int numSeconds) { wrapped.setCommandTimeout(numSeconds); } @Override public int getCommandTimeout() { return wrapped.getCommandTimeout(); } @Override public void resetTimeout(TIMEOUT_RESET_VALUE value) { wrapped.resetTimeout(value); } @Override public ModelNode execute(ModelNode mn, String description) throws CommandLineException, IOException { return wrapped.execute(mn, description); } @Override public ModelNode execute(Operation op, String description) throws CommandLineException, IOException { return wrapped.execute(op, description); } } private final CommandContext ctx; private final ExecutorService executorService = Executors.newSingleThreadExecutor(); private Future<?> handlerTask; // public for testing purpose. public CommandExecutor(CommandContext ctx) { this.ctx = ctx; } ModelNode execute(Operation op, int timeout, TimeUnit unit) throws CommandLineException, InterruptedException, ExecutionException, TimeoutException, IOException { ModelControllerClient client = ctx.getModelControllerClient(); if (client == null) { throw new CommandLineException("CLI not connected"); } if (timeout <= 0) { //Synchronous return client.execute(op); } else { // Guarded execution Future<ModelNode> task = client.executeAsync(op, OperationMessageHandler.DISCARD); try { return task.get(timeout, unit); } catch (TimeoutException ex) { cancelTask(task); throw ex; } } } // Public for testing purpose. public void execute(CommandHandler handler, int timeout, TimeUnit unit) throws CommandLineException, InterruptedException, ExecutionException, TimeoutException { if (timeout <= 0) { //Synchronous handler.handle(ctx); } else { // Guarded execution TimeoutCommandContext context = new TimeoutCommandContext(ctx); Future<Void> task = executorService.submit(() -> { handler.handle(context); return null; }); try { task.get(timeout, unit); } catch (TimeoutException ex) { // First make the context unusable context.timeout(); // Then cancel the task. task.cancel(true); throw ex; } } } private static void cancelTask(Future<?> task) { if (task != null && !(task.isDone() && task.isCancelled())) { try { task.cancel(true); } catch (Exception cex) { // XXX OK, task could be already canceled or done. } } } void cancel() { executorService.shutdownNow(); } // FOR TESTING PURPOSE ONLY public Future<?> getLastTask() { return handlerTask; } }