Package com.xebialabs.overthere.spi

Source Code of com.xebialabs.overthere.spi.BaseOverthereConnection

/*
* Copyright (c) 2008-2014, XebiaLabs B.V., All rights reserved.
*
*
* Overthere is licensed under the terms of the GPLv2
* <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most XebiaLabs Libraries.
* There are special exceptions to the terms and conditions of the GPLv2 as it is applied to
* this software, see the FLOSS License Exception
* <http://github.com/xebialabs/overthere/blob/master/LICENSE>.
*
* This program is free software; you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Foundation; version 2
* of the License.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this
* program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth
* Floor, Boston, MA 02110-1301  USA
*/
package com.xebialabs.overthere.spi;

import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.xebialabs.overthere.CmdLine;
import com.xebialabs.overthere.ConnectionOptions;
import com.xebialabs.overthere.OperatingSystemFamily;
import com.xebialabs.overthere.OverthereConnection;
import com.xebialabs.overthere.OverthereExecutionOutputHandler;
import com.xebialabs.overthere.OverthereFile;
import com.xebialabs.overthere.OverthereProcess;
import com.xebialabs.overthere.OverthereProcessOutputHandler;
import com.xebialabs.overthere.RuntimeIOException;

import static com.xebialabs.overthere.util.OverthereUtils.checkNotNull;
import static com.xebialabs.overthere.ConnectionOptions.*;
import static com.xebialabs.overthere.util.ConsoleOverthereExecutionOutputHandler.syserrHandler;
import static com.xebialabs.overthere.util.ConsoleOverthereExecutionOutputHandler.sysoutHandler;
import static com.xebialabs.overthere.util.OverthereProcessOutputHandlerWrapper.wrapStderr;
import static com.xebialabs.overthere.util.OverthereProcessOutputHandlerWrapper.wrapStdout;
import static com.xebialabs.overthere.util.OverthereUtils.closeQuietly;
import static java.lang.String.format;

/**
* A connection on a host (local or remote) on which to manipulate files and execute commands.
* <p/>
* All methods in this interface may throw a {@link com.xebialabs.overthere.RuntimeIOException} if an error occurs.
* Checked {@link java.io.IOException IOExceptions} are never thrown.
*/
public abstract class BaseOverthereConnection implements OverthereConnection {

    private static Logger logger = LoggerFactory.getLogger(BaseOverthereConnection.class);
    protected final String protocol;
    protected final ConnectionOptions options;
    protected final AddressPortMapper mapper;
    protected final OperatingSystemFamily os;
    protected final int connectionTimeoutMillis;
    protected final boolean canStartProcess;
    protected final String temporaryDirectoryPath;
    protected final boolean deleteTemporaryDirectoryOnDisconnect;
    protected final int temporaryFileCreationRetries;
    protected final String temporaryFileHolderDirectoryNamePrefix;
    protected final List<OverthereFile> temporaryFileHolderDirectories = new ArrayList<OverthereFile>();
    protected final int streamBufferSize;
    protected int temporaryFileHolderDirectoryNameSuffix = 0;
    protected OverthereFile workingDirectory;
    protected Random random = new Random();
    private volatile Boolean isClosed;
    private Throwable openStack;

    protected BaseOverthereConnection(final String protocol, final ConnectionOptions options, final AddressPortMapper mapper, final boolean canStartProcess) {
        this.protocol = checkNotNull(protocol, "Cannot create OverthereConnection with null protocol");
        this.options = checkNotNull(options, "Cannot create OverthereConnection with null options");
        this.mapper = checkNotNull(mapper, "Cannot create OverthereConnection with null address-port mapper");
        this.os = options.getEnum(OPERATING_SYSTEM, OperatingSystemFamily.class);
        this.connectionTimeoutMillis = options.getInteger(CONNECTION_TIMEOUT_MILLIS, CONNECTION_TIMEOUT_MILLIS_DEFAULT);
        this.canStartProcess = canStartProcess;
        this.temporaryDirectoryPath = options.get(TEMPORARY_DIRECTORY_PATH, os.getDefaultTemporaryDirectoryPath());
        this.deleteTemporaryDirectoryOnDisconnect = options.getBoolean(TEMPORARY_DIRECTORY_DELETE_ON_DISCONNECT, TEMPORARY_DIRECTORY_DELETE_ON_DISCONNECT_DEFAULT);
        this.temporaryFileCreationRetries = options.getInteger(TEMPORARY_FILE_CREATION_RETRIES, TEMPORARY_FILE_CREATION_RETRIES_DEFAULT);
        this.temporaryFileHolderDirectoryNamePrefix = "ot-" + (new SimpleDateFormat("yyyyMMdd'T'HHmmssSSS")).format(new Date());
        this.streamBufferSize = options.getInteger(REMOTE_COPY_BUFFER_SIZE, REMOTE_COPY_BUFFER_SIZE_DEFAULT);
    }

    protected void connected() {
        this.isClosed = Boolean.FALSE;
        this.openStack = new Throwable("Opened here...");
    }


    /**
     * Return the OS family of the host.
     *
     * @return the OS family
     */
    @Override
    public OperatingSystemFamily getHostOperatingSystem() {
        return os;
    }

    /**
     * Closes the connection. Depending on the {@link ConnectionOptions#TEMPORARY_DIRECTORY_DELETE_ON_DISCONNECT}
     * connection option, deletes all temporary files that have been created on the host.
     */
    @Override
    public final void close() {
        try {
            if (deleteTemporaryDirectoryOnDisconnect) {
                deleteConnectionTemporaryDirectory();
            }

            doClose();

            closeQuietly(mapper);

            logger.info("Disconnected from {}", this);
        } finally {
            isClosed = Boolean.TRUE;
        }
    }

    /**
     * To be overridden by a base class to implement connection specific disconnection logic.
     */
    protected abstract void doClose();

    /**
     * Creates a reference to a temporary file on the host. This file has a unique name and will be automatically
     * removed when this connection is closed. <b>N.B.:</b> The file is not actually created until a put method is
     * invoked.
     *
     * @param prefix the prefix string to be used in generating the file's name; must be at least three characters long
     * @param suffix the suffix string to be used in generating the file's name; may be <code>null</code>, in which case
     *               the suffix ".tmp" will be used
     * @return a reference to the temporary file on the host
     */
    @Override
    public final OverthereFile getTempFile(String prefix, String suffix) throws RuntimeIOException {
        checkNotNull(prefix, "prefix is null");

        if (suffix == null) {
            suffix = ".tmp";
        }

        return getTempFile(prefix + suffix);
    }

    /**
     * Creates a reference to a temporary file on the host. This file has a unique name and will be automatically
     * removed when this connection is closed. <b>N.B.:</b> The file is not actually created until a put method is
     * invoked.
     *
     * @param name the name of the temporary file. May be <code>null</code>.
     * @return a reference to the temporary file on the host
     */
    @Override
    public final synchronized OverthereFile getTempFile(String name) {
        if (name == null || name.trim().isEmpty()) {
            name = "tmp";
        }

        OverthereFile temporaryDirectory = getFile(temporaryDirectoryPath);
        RuntimeException originalExc = null;
        for (int i = 0; i <= temporaryFileCreationRetries; i++) {
            String holderName = temporaryFileHolderDirectoryNamePrefix;
            if (temporaryFileHolderDirectoryNameSuffix > 0) {
                holderName += "." + temporaryFileHolderDirectoryNameSuffix;
            }
            OverthereFile holder = getFileForTempFile(temporaryDirectory, holderName);
            if (!holder.exists()) {
                logger.trace("Creating holder directory {} for temporary file with name {}", holder, name);
                try {
                    originalExc = null;
                    holder.mkdir();
                    temporaryFileHolderDirectories.add(holder);
                    OverthereFile tempFile = holder.getFile(name);
                    logger.debug("Generated temporary file name {}", tempFile);
                    return tempFile;
                } catch(RuntimeException exc) {
                    originalExc = exc;
                    logger.debug(format("Failed to create holder directory %s - Trying with the next suffix", holder), exc);
                }
            }
            temporaryFileHolderDirectoryNameSuffix++;
        }

        if(originalExc != null) {
            throw new RuntimeIOException("Cannot generate a unique temporary file name on " + this, originalExc);
        } else {
            throw new RuntimeIOException("Cannot generate a unique temporary file name on " + this);
        }
    }

    private void deleteConnectionTemporaryDirectory() {
        for (OverthereFile d : temporaryFileHolderDirectories) {
            try {
                logger.info("Deleting temporary directory {}", d);
                d.deleteRecursively();
            } catch (RuntimeException exc) {
                logger.warn("Got exception while deleting connection temporary directory {}. Ignoring it.", d, exc);
            }
        }
    }

    /**
     * Invoked by {@link #getTempFile(String)} and {@link #getTempFile(String)} to create an
     * {@link OverthereFile} object for a file or directory in the system or connection temporary directory.
     *
     * @param parent parent of the file to create
     * @param name   name of the file to create.
     * @return the created file object
     */
    protected abstract OverthereFile getFileForTempFile(OverthereFile parent, String name);

    /**
     * Returns the working directory.
     *
     * @return the working directory, may be <code>null</code>.
     */
    @Override
    public OverthereFile getWorkingDirectory() {
        return workingDirectory;
    }

    /**
     * Sets the working directory in which commands are executed. If set to <code>null</code>, the working directory
     * that is used depends on the connection implementation.
     *
     * @param workingDirectory the working directory, may be <code>null</code>.
     */
    @Override
    public void setWorkingDirectory(OverthereFile workingDirectory) {
        this.workingDirectory = workingDirectory;
    }

    /**
     * Returns the connection options used to construct this connection.
     *
     * @return the connection options.
     */
    public ConnectionOptions getOptions() {
        return options;
    }

    @Override
    public final int execute(final CmdLine commandLine) {
        return execute(sysoutHandler(), syserrHandler(), commandLine);
    }

    @Override
    public int execute(final OverthereExecutionOutputHandler stdoutHandler, final OverthereExecutionOutputHandler stderrHandler, final CmdLine commandLine) {
        final OverthereProcess process = startProcess(commandLine);
        Thread stdoutReaderThread = null;
        Thread stderrReaderThread = null;
        final CountDownLatch latch = new CountDownLatch(2);
        try {
            stdoutReaderThread = getThread("stdout", commandLine.toString(), stdoutHandler, process.getStdout(), latch);
            stdoutReaderThread.start();

            stderrReaderThread = getThread("stderr", commandLine.toString(), stderrHandler, process.getStderr(), latch);
            stderrReaderThread.start();

            try {
                latch.await();
                return process.waitFor();
            } catch (InterruptedException exc) {
                Thread.currentThread().interrupt();

                logger.info("Execution interrupted, destroying the process.");
                process.destroy();

                throw new RuntimeIOException("Execution interrupted", exc);
            }
        } finally {
            quietlyJoinThread(stdoutReaderThread);
            quietlyJoinThread(stderrReaderThread);
        }
    }

    private void quietlyJoinThread(final Thread thread) {
        if (thread != null) {
            try {
                // interrupt the thread in case it is stuck waiting for output that will never come
                thread.interrupt();
                thread.join();
            } catch (InterruptedException ignored) {
                Thread.currentThread().interrupt();
            }
        }
    }

    private Thread getThread(final String streamName, final String commandLine, final OverthereExecutionOutputHandler outputHandler, final InputStream stream, final CountDownLatch latch) {
        Thread t = new Thread(format("%s reader", streamName)) {
            @Override
            public void run() {
                StringBuilder lineBuffer = new StringBuilder();
                InputStreamReader stdoutReader = new InputStreamReader(stream);
                latch.countDown();
                try {
                    int cInt = stdoutReader.read();
                    while (cInt > -1) {
                        char c = (char) cInt;
                        outputHandler.handleChar(c);
                        if (c != '\r' && c != '\n') {
                            lineBuffer.append(c);
                        }
                        if (c == '\n') {
                            outputHandler.handleLine(lineBuffer.toString());
                            lineBuffer.setLength(0);
                        }
                        cInt = stdoutReader.read();
                    }
                } catch (Exception exc) {
                    logger.error(format("An exception occured reading %s while executing [%s] on %s", streamName, commandLine, this), exc);
                } finally {
                    closeQuietly(stdoutReader);
                    if (lineBuffer.length() > 0) {
                        outputHandler.handleLine(lineBuffer.toString());
                    }
                }
            }
        };
        t.setDaemon(true);
        return t;
    }

    /**
     * Executes a command with its arguments.
     *
     * @param handler     the handler that will be invoked when the executed command generated output.
     * @param commandLine the command line to execute.
     * @return the exit value of the executed command. Usually 0 on successful execution.
     * @deprecated use {@link BaseOverthereConnection#execute(com.xebialabs.overthere.OverthereExecutionOutputHandler, com.xebialabs.overthere.OverthereExecutionOutputHandler, com.xebialabs.overthere.CmdLine)}
     */
    @Override
    public final int execute(final OverthereProcessOutputHandler handler, final CmdLine commandLine) {
        return execute(wrapStdout(handler), wrapStderr(handler), commandLine);
    }

    /**
     * Starts a command with its argument and returns control to the caller.
     *
     * @param commandLine the command line to execute.
     * @return an object representing the executing command or <tt>null</tt> if this is not supported by the host
     *         connection.
     */
    @Override
    public OverthereProcess startProcess(CmdLine commandLine) {
        throw new UnsupportedOperationException("Cannot start a process on " + this);
    }

    /**
     * Checks whether a process can be started on this connection.
     *
     * @return <code>true</code> if a process can be started on this connection, <code>false</code> otherwise
     */
    @Override
    public final boolean canStartProcess() {
        return canStartProcess;
    }

    /**
     * Subclasses MUST implement toString properly.
     */
    @Override
    public abstract String toString();

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        final BaseOverthereConnection that = (BaseOverthereConnection) o;

        return options.equals(that.options) && protocol.equals(that.protocol);
    }

    @Override
    public int hashCode() {
        int result = protocol.hashCode();
        result = 31 * result + options.hashCode();
        return result;
    }

    /**
     * Make sure that the connection is cleaned up. This will log error messages if the connection is collected before it is cleaned up.
     *
     * @throws Throwable
     */
    @Override
    protected void finalize() throws Throwable {
        // Only trigger if connected() method was called to set the connected state
        if (isClosed != null && !isClosed) {
            logger.error(String.format("Connection [%s] was not closed, closing automatically.", this), openStack);
            closeQuietly(this);
        }
        super.finalize();
    }
}
TOP

Related Classes of com.xebialabs.overthere.spi.BaseOverthereConnection

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.