/*
* #%L
* JavaHg
* %%
* Copyright (C) 2011 aragost Trifork ag
* %%
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
* #L%
*/
package com.aragost.javahg.internals;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.CharsetDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.ConcurrentModificationException;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import com.aragost.javahg.Changeset;
import com.aragost.javahg.DateTime;
import com.aragost.javahg.Repository;
import com.aragost.javahg.UnknownCommandException;
import com.aragost.javahg.commands.CancelledExecutionException;
import com.aragost.javahg.commands.ExecutionException;
import com.google.common.collect.Lists;
import com.google.common.io.ByteStreams;
/**
* Base class for the command classes.
*
* Each Mercurial command (e.g., "log", "commit", etc) is mapped to a command
* class, which is a subclass of this class. The command classes will provide
* methods for setting command line flags and for actually executing the
* command.
*
* Concurrency: Instances of this class should be accessed only on a single
* thread. The only exception is {@link #cancel()}.
*
* States: Normally: READY->QUEUED->RUNNING->READY. If cancelled: From READY,
* QUEUED, or RUNNING -> CANCELLING -> READY. If cancelled the executing thread
* will have a CancelledExecutionException thrown and state will be READY.
*/
public abstract class AbstractCommand {
public enum State { READY, QUEUED, RUNNING, CANCELING };
private AtomicReference<State> state = new AtomicReference<AbstractCommand.State>(State.READY);
private final List<String> cmdLine;
private final Repository repository;
private OutputChannelInputStream outputChannelStream;
private final DirectByteArrayOutputStream error = new DirectByteArrayOutputStream();
private String errorMessage;
private int returnCode = Integer.MIN_VALUE;
private int lineChannelLength;
/**
* Not null while in {@link State#RUNNING}.
*/
private volatile Server server;
protected AbstractCommand(Repository repository) {
this.repository = repository;
this.cmdLine = Lists.newArrayList(getCommandName());
}
protected AbstractCommand(Repository repository, String commandName) {
this.repository = repository;
this.cmdLine = Lists.newArrayList(commandName);
}
/**
* @return the name of this Mercurial command, i.e., "add", "log",
* etc
*/
public abstract String getCommandName();
public void cmdAppend(String option) {
this.cmdLine.add(option);
}
public void cmdAppend(String option, String arg) {
if (arg == null) {
throw new NullPointerException("cannot pass null for " + option + " flag");
}
this.cmdLine.add(option);
this.cmdLine.add(arg);
}
public void cmdAppend(String option, String[] args) {
for (String arg : args) {
cmdAppend(option, arg);
}
}
public void cmdAppend(String option, int arg) {
this.cmdLine.add(option);
this.cmdLine.add("" + arg);
}
public void cmdAppend(String option, DateTime date) {
if (date == null) {
throw new NullPointerException("cannot pass null for " + option + " flag");
}
this.cmdLine.add(option);
this.cmdLine.add(date.getHgString());
}
/**
* Launch the command and return stdout as a String.
*
* @param args
* extra command line arguments (optional).
* @return stdout as a String.
* @throws IOException
*/
protected final String launchString(String... args) {
InputStream stdout = launchStream(args);
try {
return Utils.readStream(stdout, getRepository().newDecoder());
} finally {
cleanUp();
}
}
/**
* Launch the command and return stdout as a InputStream.
*
* @param args
* extra command line arguments (optional).
* @return stdout stream
*/
protected final HgInputStream launchStream(String... args) {
clear();
changeState(State.READY, State.QUEUED, true);
try {
server = repository.getServerPool().take(this);
changeState(State.QUEUED, State.RUNNING, true);
} catch (InterruptedException e1) {
changeState(State.CANCELING, State.READY, false);
throw new CancelledExecutionException(this);
}
List<String> commandLine = new ArrayList<String>(this.cmdLine);
boolean ok = false;
getRepository().addToCommandLine(commandLine);
commandLine.addAll(Arrays.asList(args));
try {
this.outputChannelStream = server.runCommand(commandLine, this);
HgInputStream stream = new HgInputStream(outputChannelStream, this.repository.newDecoder());
ok = true;
return stream;
} catch (UnexpectedServerTerminationException e) {
if (state.get() == State.CANCELING) {
throw new CancelledExecutionException(this);
}
throw e;
} catch (IOException e) {
throw new RuntimeIOException(e);
} finally {
// If an exception is thrown there is no chance the command will
// finish normally so abort the server. E.g protocol error
if (!ok && state.get() != State.READY) {
state.set(State.READY);
repository.getServerPool().abort(server);
server = null;
}
}
}
/**
* Changes state checking for cancellation
*
* @param current
* The expected state
* @param next
* The state to change to
* @param strict
* If true a {@link ConcurrentModificationException} is thrown if
* in unexpected state
* @throws CancelledExecutionException
* If in cancelled state
* @throws ConcurrentModificationException
* If in another unexpected state and if strict is true
*/
private void changeState(State current, State next, boolean strict) {
if (!state.compareAndSet(current, next)) {
if (state.compareAndSet(State.CANCELING, State.READY)) {
throw new CancelledExecutionException(this);
}
if (strict) {
throw new ConcurrentModificationException("Unexpected command state");
}
}
}
protected final LineIterator launchIterator(String... args) {
return new LineIterator(launchStream(args));
}
/**
* Open the output stream again after sending input to the command
* server. When the server alternates between sending output on
* the 'o' channel and reading input lines, the output channel
* will run dry several times. Call this method after sending
* input to the command server in response to reading from the 'L'
* channel. New output from the server will then appear on the
* output channel.
*/
public void reopenOutputChannelStream() {
this.outputChannelStream.reopen();
}
/**
* Send input line to command server in response to reading a
* block on the 'L' channel.
*
* @param s
* line of input.
*/
public void sendLine(String s) {
if (this.lineChannelLength == 0) {
throw new IllegalStateException("No input expected");
}
server.sendLine(s);
this.lineChannelLength = 0;
}
/**
* @return true if we have read a 'L' channel from the server.
*/
public boolean needsInputLine() {
return this.lineChannelLength > 0;
}
/**
* Finish the request by consuming any remaining input on the
* output channel from the server. The server wont respond to new
* commands until this is done.
*/
void cleanUp() {
if (this.outputChannelStream != null) {
try {
Utils.consumeAll(this.outputChannelStream);
} catch (IOException e) {
throw new RuntimeIOException(e);
}
}
}
protected void clear() {
this.error.reset();
this.errorMessage = null;
this.returnCode = Integer.MIN_VALUE;
}
/**
* Check if the command ended with a zero return code. Subclasses
* can override this to accept other return codes as successful.
*
* @return true if the command ended successfully.
*/
protected boolean isSuccessful() {
return getReturnCode() == 0;
}
/**
* @return data read on the 'e' channel
*/
public String getErrorString() {
String errorMessage = this.errorMessage;
if (errorMessage == null) {
CharsetDecoder decoder = getRepository().newDecoder();
this.errorMessage = errorMessage =
Utils.decodeBytes(error.buffer(), 0, error.size(), decoder);
}
return errorMessage;
}
/**
* @return the return code read from the 'r' channel
*/
public int getReturnCode() {
if (this.returnCode == Integer.MIN_VALUE) {
throw new IllegalStateException("cmdserver is still executing request");
}
return this.returnCode;
}
/**
* @return the {@link Repository} associated with this command.
*/
public Repository getRepository() {
return repository;
}
@Override
public String toString() {
return getCommandName();
}
/**
* @param lineChannelLength
* the line length requested by the server on the 'L'
* channel.
*/
void setLineChannelLength(int lineChannelLength) {
this.lineChannelLength = lineChannelLength;
}
/**
* Add to the stderr data stored in this command.
*
* @param cin
* stderr of the command server.
* @throws IOException
*/
void addToError(BlockInputStream cin) throws IOException {
ByteStreams.copy(cin, this.error);
this.errorMessage = null;
}
protected void withDebugAndChangesetStyle() {
withDebugFlag();
cmdAppend("--style", Changeset.CHANGESET_STYLE_PATH);
}
protected void withDebugFlag() {
cmdAppend("--debug");
}
/**
* Called exactly once when this command finishes executing. The server is
* freed and the state is changed to DONE.
*
* @param returnCode The exit code of the completed command
*/
final void handleReturnCode(int returnCode) {
this.returnCode = returnCode;
server.clearCurrentCommand(this);
repository.getServerPool().put(server);
server = null;
changeState(State.RUNNING, State.READY, false);
if (returnCode != 0 && returnCode != 1) {
// This can for example happens for an unknown command
String errorString = getErrorString();
if (errorString.startsWith("hg: unknown command '")) {
throw new UnknownCommandException(this);
}
}
doneHook();
if (!isSuccessful()) {
throw new ExecutionException(this);
}
}
/**
* Cancel a running command. May be called from a different thread. Returns
* immediately. The thread executing the command should return soon after
* with a {@link CancelledExecutionException} thrown.
*/
public final void cancel() {
State oldState = state.getAndSet(State.CANCELING);
if (oldState != State.READY) {
// There is a small chance the final state of a command will be
// CANCELLING rather than DONE but this doesn't actually matter.
Server server = this.server;
Process process;
if (server != null && (process = server.getProcess()) != null) {
process.destroy();
}
}
}
/**
* @return The current state of the command.
*/
State getState() {
return state.get();
}
/**
* This method is called when the processing of a command is
* finished. More precise it is called just after the 'r' channel
* is read
* <p>
* It can be overridden in subclasses.
*/
protected void doneHook() {
}
private static class DirectByteArrayOutputStream extends ByteArrayOutputStream {
private DirectByteArrayOutputStream() {
super(256);
}
byte[] buffer() {
return super.buf;
}
}
}