/*
* JBoss, Home of Professional Open Source.
* Copyright 2012, 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.controller.remote;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ATTACHED_STREAMS;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.CANCELLED;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.FAILED;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.FAILURE_DESCRIPTION;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OUTCOME;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RESPONSE_HEADERS;
import static org.jboss.as.protocol.mgmt.ProtocolUtils.expectHeader;
import java.io.DataInput;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.security.Principal;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.security.auth.Subject;
import org.jboss.as.controller.ModelController;
import org.jboss.as.controller.client.MessageSeverity;
import org.jboss.as.controller.client.OperationAttachments;
import org.jboss.as.controller.client.OperationMessageHandler;
import org.jboss.as.controller.client.OperationResponse;
import org.jboss.as.controller.client.impl.AbstractDelegatingAsyncFuture;
import org.jboss.as.controller.client.impl.ModelControllerProtocol;
import org.jboss.as.controller.client.impl.OperationResponseProxy;
import org.jboss.as.controller.logging.ControllerLogger;
import org.jboss.as.protocol.StreamUtils;
import org.jboss.as.protocol.mgmt.AbstractManagementRequest;
import org.jboss.as.protocol.mgmt.ActiveOperation;
import org.jboss.as.protocol.mgmt.FlushableDataOutput;
import org.jboss.as.protocol.mgmt.ManagementChannelAssociation;
import org.jboss.as.protocol.mgmt.ManagementChannelHandler;
import org.jboss.as.protocol.mgmt.ManagementProtocol;
import org.jboss.as.protocol.mgmt.ManagementRequestContext;
import org.jboss.as.protocol.mgmt.ManagementRequestHandler;
import org.jboss.as.protocol.mgmt.ManagementRequestHandlerFactory;
import org.jboss.as.protocol.mgmt.ManagementRequestHeader;
import org.jboss.as.protocol.mgmt.ManagementResponseHeader;
import org.jboss.dmr.ModelNode;
import org.jboss.threads.AsyncFuture;
import org.wildfly.security.manager.WildFlySecurityManager;
/**
* Base implementation for the transactional protocol.
*
* @author Emanuel Muckenhuber
* @author <a href="mailto:darran.lofthouse@jboss.com">Darran Lofthouse</a>
*/
class TransactionalProtocolClientImpl implements ManagementRequestHandlerFactory, TransactionalProtocolClient {
private static final File javaTempDir = new File(WildFlySecurityManager.getPropertyPrivileged("java.io.tmpdir", null));
private final File tempDir;
private final ManagementChannelAssociation channelAssociation;
public TransactionalProtocolClientImpl(final ManagementChannelAssociation channelAssociation) {
assert channelAssociation != null;
this.channelAssociation = channelAssociation;
final File temp = channelAssociation.getAttachments().getAttachment(ManagementChannelHandler.TEMP_DIR);
if (temp != null && temp.isDirectory()) {
tempDir = temp;
} else {
tempDir = javaTempDir;
}
}
/** {@inheritDoc} */
@Override
public ManagementRequestHandler<?, ?> resolveHandler(RequestHandlerChain handlers, ManagementRequestHeader header) {
final byte operationType = header.getOperationId();
if (operationType == ModelControllerProtocol.HANDLE_REPORT_REQUEST) {
return new HandleReportRequestHandler();
} else if (operationType == ModelControllerProtocol.GET_INPUTSTREAM_REQUEST) {
return ReadAttachmentInputStreamRequestHandler.INSTANCE;
}
return handlers.resolveNext();
}
@Override
public AsyncFuture<OperationResponse> execute(TransactionalOperationListener<Operation> listener, ModelNode operation, OperationMessageHandler messageHandler, OperationAttachments attachments) throws IOException {
final Operation wrapper = TransactionalProtocolHandlers.wrap(operation, messageHandler, attachments);
return execute(listener, wrapper);
}
@Override
public <T extends Operation> AsyncFuture<OperationResponse> execute(TransactionalOperationListener<T> listener, T operation) throws IOException {
final Subject subject = SecurityActions.getSubject();
final ExecuteRequestContext context = new ExecuteRequestContext(new OperationWrapper<T>(listener, operation), subject, tempDir);
final ActiveOperation<OperationResponse, ExecuteRequestContext> op = channelAssociation.initializeOperation(context, context);
final AtomicBoolean cancelSent = new AtomicBoolean();
final AsyncFuture<OperationResponse> result = new AbstractDelegatingAsyncFuture<OperationResponse>(op.getResult()) {
@Override
public synchronized void asyncCancel(boolean interruptionDesired) {
if (!cancelSent.get()) {
try {
// Execute
channelAssociation.executeRequest(op, new CompleteTxRequest(ModelControllerProtocol.PARAM_ROLLBACK, channelAssociation));
cancelSent.set(true);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
};
context.initialize(result);
channelAssociation.executeRequest(op, new ExecuteRequest());
return result;
}
/**
* Request for the the remote {@link TransactionalProtocolOperationHandler.ExecuteRequestHandler}.
*
* The required response is either a:
* - {@link org.jboss.as.controller.client.impl.ModelControllerProtocol#PARAM_OPERATION_FAILED}, which will complete the operation right away
* - or {@link org.jboss.as.controller.client.impl.ModelControllerProtocol#PARAM_OPERATION_PREPARED}
*/
private class ExecuteRequest extends AbstractManagementRequest<OperationResponse, ExecuteRequestContext> {
@Override
public byte getOperationType() {
return ModelControllerProtocol.EXECUTE_TX_REQUEST;
}
@Override
public void sendRequest(final ActiveOperation.ResultHandler<OperationResponse> resultHandler,
final ManagementRequestContext<ExecuteRequestContext> context) throws IOException {
ControllerLogger.MGMT_OP_LOGGER.tracef("sending ExecuteRequest for %d", context.getOperationId());
// WFLY-3090 Protect the communication channel from getting closed due to administrative
// cancellation of the management op by using a separate thread to send
context.executeAsync(new ManagementRequestContext.AsyncTask<ExecuteRequestContext>() {
@Override
public void execute(ManagementRequestContext<ExecuteRequestContext> context) throws Exception {
sendRequestInternal(resultHandler, context);
}
}, false);
}
@Override
protected void sendRequest(final ActiveOperation.ResultHandler<OperationResponse> resultHandler,
final ManagementRequestContext<ExecuteRequestContext> context,
final FlushableDataOutput output) throws IOException {
ControllerLogger.MGMT_OP_LOGGER.tracef("transmitting ExecuteRequest for %d", context.getOperationId());
// Write the operation
final ExecuteRequestContext executionContext = context.getAttachment();
final List<InputStream> streams = executionContext.getInputStreams();
final ModelNode operation = executionContext.getOperation();
int inputStreamLength = 0;
if (streams != null) {
inputStreamLength = streams.size();
}
output.write(ModelControllerProtocol.PARAM_OPERATION);
operation.writeExternal(output);
output.write(ModelControllerProtocol.PARAM_INPUTSTREAMS_LENGTH);
output.writeInt(inputStreamLength);
final Boolean sendSubject = channelAssociation.getAttachments().getAttachment(SEND_SUBJECT);
if (sendSubject != null && sendSubject) {
final Subject subject = context.getAttachment().getSerializableSubject();
writeSubject(output, subject);
}
}
@Override
public void handleRequest(final DataInput input, final ActiveOperation.ResultHandler<OperationResponse> resultHandler, final ManagementRequestContext<ExecuteRequestContext> context) throws IOException {
ControllerLogger.MGMT_OP_LOGGER.tracef("received response to ExecuteRequest for %d", context.getOperationId());
final byte responseType = input.readByte();
final ModelNode response = new ModelNode();
response.readExternal(input);
// If not prepared the operation failed
final boolean prepared = responseType == ModelControllerProtocol.PARAM_OPERATION_PREPARED;
final ExecuteRequestContext executeRequestContext = context.getAttachment();
if(prepared) {
executeRequestContext.operationPrepared(new ModelController.OperationTransaction() {
@Override
public void rollback() {
done(false);
}
@Override
public void commit() {
done(true);
}
private void done(boolean commit) {
final byte status = commit ? ModelControllerProtocol.PARAM_COMMIT : ModelControllerProtocol.PARAM_ROLLBACK;
try {
// Send the CompleteTxRequest
channelAssociation.executeRequest(context.getOperationId(), new CompleteTxRequest(status, channelAssociation));
} catch (Exception e) {
resultHandler.failed(e);
}
}
}, response);
} else {
// Failed
executeRequestContext.operationFailed(response);
resultHandler.done(OperationResponse.Factory.createSimple(response));
}
}
private void sendRequestInternal(ActiveOperation.ResultHandler<OperationResponse> resultHandler,
ManagementRequestContext<ExecuteRequestContext> context) throws IOException {
super.sendRequest(resultHandler, context);
}
}
/**
* Signal the remote controller to either commit or rollback. The response has to be a
* {@link org.jboss.as.controller.client.impl.ModelControllerProtocol#PARAM_OPERATION_COMPLETED}.
*/
private static class CompleteTxRequest extends AbstractManagementRequest<OperationResponse, ExecuteRequestContext> {
private final byte status;
private final ManagementChannelAssociation channelAssociation;
private CompleteTxRequest(byte status, ManagementChannelAssociation channelAssociation) {
this.status = status;
this.channelAssociation = channelAssociation;
}
@Override
public byte getOperationType() {
return ModelControllerProtocol. COMPLETE_TX_REQUEST;
}
@Override
public void sendRequest(final ActiveOperation.ResultHandler<OperationResponse> resultHandler,
final ManagementRequestContext<ExecuteRequestContext> context) throws IOException {
ControllerLogger.MGMT_OP_LOGGER.tracef("sending CompleteTxRequest for %d", context.getOperationId());
context.executeAsync(new ManagementRequestContext.AsyncTask<ExecuteRequestContext>() {
@Override
public void execute(ManagementRequestContext<ExecuteRequestContext> context) throws Exception {
sendRequestInternal(resultHandler, context);
}
}, false);
}
@Override
protected void sendRequest(final ActiveOperation.ResultHandler<OperationResponse> resultHandler, final ManagementRequestContext<ExecuteRequestContext> context, final FlushableDataOutput output) throws IOException {
ControllerLogger.MGMT_OP_LOGGER.tracef("transmitting CompleteTxRequest (%s) for %d", status != ModelControllerProtocol.PARAM_ROLLBACK, context.getOperationId());
output.write(status);
}
@Override
public void handleRequest(final DataInput input, final ActiveOperation.ResultHandler<OperationResponse> resultHandler, final ManagementRequestContext<ExecuteRequestContext> context) throws IOException {
ControllerLogger.MGMT_OP_LOGGER.tracef("received response to CompleteTxRequest (%s) for %d", status != ModelControllerProtocol.PARAM_ROLLBACK, context.getOperationId());
// We only accept operationCompleted responses
expectHeader(input, ModelControllerProtocol.PARAM_OPERATION_COMPLETED);
final ModelNode responseNode = new ModelNode();
responseNode.readExternal(input);
// Complete the operation
resultHandler.done(createOperationResponse(responseNode, channelAssociation, context.getOperationId()));
}
private void sendRequestInternal(ActiveOperation.ResultHandler<OperationResponse> resultHandler,
ManagementRequestContext<ExecuteRequestContext> context) throws IOException {
super.sendRequest(resultHandler, context);
}
}
/**
* Handles {@link org.jboss.as.controller.client.OperationMessageHandler#handleReport(org.jboss.as.controller.client.MessageSeverity, String)} calls
* done in the remote target controller
*/
private static class HandleReportRequestHandler implements ManagementRequestHandler<ModelNode, ExecuteRequestContext> {
@Override
public void handleRequest(final DataInput input, final ActiveOperation.ResultHandler<ModelNode> resultHandler, final ManagementRequestContext<ExecuteRequestContext> context) throws IOException {
expectHeader(input, ModelControllerProtocol.PARAM_MESSAGE_SEVERITY);
final MessageSeverity severity = Enum.valueOf(MessageSeverity.class, input.readUTF());
expectHeader(input, ModelControllerProtocol.PARAM_MESSAGE);
final String message = input.readUTF();
expectHeader(input, ManagementProtocol.REQUEST_END);
final ExecuteRequestContext requestContext = context.getAttachment();
// perhaps execute async
final OperationMessageHandler handler = requestContext.getMessageHandler();
handler.handleReport(severity, message);
}
}
/**
* Handles reads on the inputstreams returned by {@link org.jboss.as.controller.client.OperationAttachments#getInputStreams()}
* done in the remote target controller
*/
private static class ReadAttachmentInputStreamRequestHandler implements ManagementRequestHandler<ModelNode, ExecuteRequestContext> {
static final ReadAttachmentInputStreamRequestHandler INSTANCE = new ReadAttachmentInputStreamRequestHandler();
@Override
public void handleRequest(final DataInput input, final ActiveOperation.ResultHandler<ModelNode> resultHandler,
final ManagementRequestContext<ExecuteRequestContext> context) throws IOException {
// Read the inputStream index
expectHeader(input, ModelControllerProtocol.PARAM_INPUTSTREAM_INDEX);
final int index = input.readInt();
context.executeAsync(new ManagementRequestContext.AsyncTask<ExecuteRequestContext>() {
@Override
public void execute(final ManagementRequestContext<ExecuteRequestContext> context) throws Exception {
final ExecuteRequestContext exec = context.getAttachment();
final ManagementRequestHeader header = ManagementRequestHeader.class.cast(context.getRequestHeader());
final ManagementResponseHeader response = new ManagementResponseHeader(header.getVersion(), header.getRequestId(), null);
final InputStream is = exec.getAttachments().getInputStreams().get(index);
try {
final File temp = copyStream(is, exec.tempDir);
try {
final FlushableDataOutput output = context.writeMessage(response);
try {
output.writeByte(ModelControllerProtocol.PARAM_INPUTSTREAM_LENGTH);
output.writeInt((int) temp.length()); // the int is required by the protocol
output.writeByte(ModelControllerProtocol.PARAM_INPUTSTREAM_CONTENTS);
final FileInputStream fis = new FileInputStream(temp);
try {
StreamUtils.copyStream(fis, output);
fis.close();
} finally {
StreamUtils.safeClose(fis);
}
output.writeByte(ManagementProtocol.RESPONSE_END);
output.close();
} finally {
StreamUtils.safeClose(output);
}
} finally {
temp.delete();
}
} finally {
// the caller is responsible for closing the input streams
// StreamUtils.safeClose(is);
}
}
});
}
protected File copyStream(final InputStream is, final File tempDir) throws IOException {
final File temp = File.createTempFile("upload", "temp", tempDir);
if (is != null) {
final FileOutputStream os = new FileOutputStream(temp);
try {
StreamUtils.copyStream(is, os);
os.close();
} finally {
StreamUtils.safeClose(os);
}
}
return temp;
}
}
static class ExecuteRequestContext implements ActiveOperation.CompletedCallback<OperationResponse> {
final OperationWrapper<?> wrapper;
final AtomicBoolean completed = new AtomicBoolean(false);
final Subject subject;
final File tempDir;
ExecuteRequestContext(OperationWrapper<?> operationWrapper, Subject subject, File tempDir) {
this.wrapper = operationWrapper;
this.subject = subject;
this.tempDir = tempDir;
}
void initialize(final AsyncFuture<OperationResponse> result) {
wrapper.future = result;
}
OperationMessageHandler getMessageHandler() {
return wrapper.getMessageHandler();
}
ModelNode getOperation() {
return wrapper.getOperation();
}
OperationAttachments getAttachments() {
return wrapper.getAttachments();
}
List<InputStream> getInputStreams() {
final OperationAttachments attachments = getAttachments();
if(attachments == null) {
return Collections.emptyList();
}
return attachments.getInputStreams();
}
Subject getSerializableSubject() {
if (subject != null) {
Subject toSend = new Subject();
Set<Principal> principals = toSend.getPrincipals();
for (Principal current : subject.getPrincipals()) {
if (current instanceof Serializable) {
principals.add(current);
}
}
toSend.setReadOnly();
return toSend;
}
return null;
}
@Override
public synchronized void completed(final OperationResponse result) {
if(completed.compareAndSet(false, true)) {
wrapper.completed(result);
}
}
@Override
public void failed(Exception e) {
operationFailed(getFailureResponse(FAILED, e.getMessage()));
}
@Override
public void cancelled() {
operationFailed(getResponse(CANCELLED));
}
synchronized void operationFailed(final ModelNode response) {
if(completed.compareAndSet(false, true)) {
wrapper.failed(response);
}
}
synchronized void operationPrepared(final ModelController.OperationTransaction transaction, final ModelNode result) {
wrapper.prepared(transaction, result);
}
}
private static class OperationWrapper<T extends Operation> {
private final T operation;
private final TransactionalOperationListener<T> listener;
private AsyncFuture<OperationResponse> future;
OperationWrapper(TransactionalOperationListener<T> listener, T operation) {
this.listener = listener;
this.operation = operation;
}
OperationMessageHandler getMessageHandler() {
return operation.getMessageHandler();
}
ModelNode getOperation() {
return operation.getOperation();
}
OperationAttachments getAttachments() {
return operation.getAttachments();
}
void prepared(final ModelController.OperationTransaction transaction, final ModelNode result) {
final PreparedOperation<T> preparedOperation = new PreparedOperationImpl<T>(operation, result, future, transaction);
listener.operationPrepared(preparedOperation);
}
void completed(final OperationResponse response) {
listener.operationComplete(operation, response);
}
void failed(final ModelNode response) {
listener.operationFailed(operation, response);
}
}
private static OperationResponse createOperationResponse(ModelNode simpleResponse, ManagementChannelAssociation channelAssociation, int operationId) {
final ModelNode streamHeader = simpleResponse.hasDefined(RESPONSE_HEADERS) && simpleResponse.get(RESPONSE_HEADERS).hasDefined(ATTACHED_STREAMS)
? simpleResponse.get(RESPONSE_HEADERS, ATTACHED_STREAMS)
: null;
if (streamHeader != null && streamHeader.asInt() > 0) {
return OperationResponseProxy.create(simpleResponse, channelAssociation, operationId, streamHeader);
} else {
return OperationResponse.Factory.createSimple(simpleResponse);
}
}
static class PreparedOperationImpl<T extends Operation> implements PreparedOperation<T> {
private final T operation;
private final ModelNode preparedResult;
private final AsyncFuture<OperationResponse> finalResult;
private final ModelController.OperationTransaction transaction;
protected PreparedOperationImpl(T operation, ModelNode preparedResult, AsyncFuture<OperationResponse> finalResult, ModelController.OperationTransaction transaction) {
assert finalResult != null : "null result";
this.operation = operation;
this.preparedResult = preparedResult;
this.finalResult = finalResult;
this.transaction = transaction;
}
@Override
public T getOperation() {
return operation;
}
@Override
public ModelNode getPreparedResult() {
return preparedResult;
}
@Override
public boolean isFailed() {
return false;
}
@Override
public boolean isTimedOut() {
return false;
}
@Override
public boolean isDone() {
return finalResult.isDone();
}
@Override
public AsyncFuture<OperationResponse> getFinalResult() {
return finalResult;
}
@Override
public void commit() {
transaction.commit();
}
@Override
public void rollback() {
transaction.rollback();
}
}
static ModelNode getFailureResponse(final String outcome, final String message) {
final ModelNode response = new ModelNode();
response.get(OUTCOME).set(outcome);
if(message != null) response.get(FAILURE_DESCRIPTION).set(message);
return response;
}
static ModelNode getResponse(final String outcome) {
return getFailureResponse(outcome, null);
}
static void writeSubject(final FlushableDataOutput output, final Subject subject) throws IOException {
SubjectProtocolUtil.write(output, subject);
}
}