interactivespaces.liveactivity.runtime.standalone.StandaloneActivityRunner.java Source code

Java tutorial

Introduction

Here is the source code for interactivespaces.liveactivity.runtime.standalone.StandaloneActivityRunner.java

Source

/*
 * Copyright (C) 2015 Google 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 interactivespaces.liveactivity.runtime.standalone;

import interactivespaces.InteractiveSpacesException;
import interactivespaces.activity.ActivityRuntimeStartupType;
import interactivespaces.activity.ActivityState;
import interactivespaces.activity.binary.NativeActivityRunnerFactory;
import interactivespaces.activity.binary.SimpleNativeActivityRunnerFactory;
import interactivespaces.activity.component.ActivityComponentFactory;
import interactivespaces.activity.component.CoreExistingActivityComponentFactory;
import interactivespaces.activity.component.route.MessageRouterActivityComponent;
import interactivespaces.configuration.Configuration;
import interactivespaces.configuration.SimplePropertyFileSingleConfigurationStorageManager;
import interactivespaces.configuration.SingleConfigurationStorageManager;
import interactivespaces.domain.support.ActivityDescription;
import interactivespaces.domain.support.ActivityDescriptionReader;
import interactivespaces.domain.support.JdomActivityDescriptionReader;
import interactivespaces.evaluation.ExpressionEvaluator;
import interactivespaces.evaluation.SimpleExpressionEvaluator;
import interactivespaces.liveactivity.runtime.InternalLiveActivityFilesystem;
import interactivespaces.liveactivity.runtime.LiveActivityRunner;
import interactivespaces.liveactivity.runtime.LiveActivityRunnerFactory;
import interactivespaces.liveactivity.runtime.LiveActivityRunnerListener;
import interactivespaces.liveactivity.runtime.SimpleLiveActivityFilesystem;
import interactivespaces.liveactivity.runtime.configuration.LiveActivityConfiguration;
import interactivespaces.liveactivity.runtime.configuration.StandardLiveActivityConfiguration;
import interactivespaces.liveactivity.runtime.domain.ActivityInstallationStatus;
import interactivespaces.liveactivity.runtime.domain.InstalledLiveActivity;
import interactivespaces.liveactivity.runtime.domain.pojo.SimpleInstalledLiveActivity;
import interactivespaces.liveactivity.runtime.standalone.messaging.StandaloneMessageRouter;
import interactivespaces.liveactivity.runtime.standalone.stubs.StandaloneSpaceController;
import interactivespaces.resource.Version;
import interactivespaces.system.InteractiveSpacesEnvironment;
import interactivespaces.util.io.FileSupport;
import interactivespaces.util.io.FileSupportImpl;

import com.google.common.io.Closeables;

import org.apache.commons.lang.SystemUtils;
import org.apache.commons.logging.Log;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.channels.FileLock;
import java.util.Date;
import java.util.Map;

/**
 * Standalone activity runner class.
 *
 * @author Trevor Pering
 */
public class StandaloneActivityRunner {

    /**
     * Mask to use for creating a random UUID.
     */
    public static final int RANDOM_UUID_MAX = 0x10000000;

    /**
     * Number of threads in the thread pool.
     */
    public static final int THREAD_POOL_SIZE = 100;

    /**
     * Config filename to use for activity specific configuration.
     */
    public static final String ACTIVITY_SPECIFIC_CONFIG_FILE_NAME = "standalone.conf";

    /**
     * Config filename to use for activity specific configuration.
     */
    public static final String LOCAL_CONFIG_FILE_NAME = "local.conf";

    /**
     * Config filename for the included activity configuration.
     */
    public static final String ACTIVITY_CONFIG_FILE_NAME = "activity.conf";

    /**
     * Filename to use for a running instance lock.
     */
    private static final String LOCK_FILE_NAME = "instancelock";

    /**
     * Maximum number of standalone instances to allow.
     */
    private static final int MAX_INSTANCE_COUNT = 10;

    /**
     * File support instance to use.
     */
    private final FileSupport fileSupport = FileSupportImpl.INSTANCE;

    /**
     * Root directory for the individual activity.
     */
    private File activityRootDir;

    /**
     * Instance suffix for handling multiple instances run in the same directory.
     */
    private String instanceSuffix;

    /**
     * {@code true} if this is the primary (first) instance running in the same directory.
     */
    private boolean isPrimaryInstance;

    /**
     * {@code true} (default) if the standalone router should be used.
     */
    private boolean useStandaloneRouter = true;

    /**
     * Space controller instance.
     */
    private StandaloneSpaceController controller;

    /**
     * Factory for native activity runners.
     */
    private NativeActivityRunnerFactory nativeActivityRunnerFactory;

    /**
     * Active activity under test.
     */
    private LiveActivityRunner activeActivity;

    /**
     * Activity filesystem for activity.
     */
    private InternalLiveActivityFilesystem activityFilesystem;

    /**
     * Path name for trace filters, if any.
     */
    private String traceFilterPath;

    /**
     * Path name for trace playback file.
     */
    private String tracePlaybackPath;

    /**
     * Path name for trace check file.
     */
    private String traceCheckPath;

    /**
     * Path name for send path.
     */
    private String traceSendPath;

    /**
     * Mutable local config file for this activity. Has default, but can be set.
     */
    private File localConfigFile = new File(LOCAL_CONFIG_FILE_NAME);

    /**
     * Mutable local config file for this activity. Has default, but can be set.
     */
    private File activityConfigFile = new File(ACTIVITY_SPECIFIC_CONFIG_FILE_NAME);

    /**
     * Message router to use for this standalone activity.
     */
    private StandaloneMessageRouter cecRouter;

    /**
     * The controller activity factory.
     */
    private final LiveActivityRunnerFactory liveActivityRunnerFactory;

    /**
     * Space environment.
     */
    private final InteractiveSpacesEnvironment spaceEnvironment;

    /**
     * The activity component factory.
     */
    private ActivityComponentFactory activityComponentFactory;

    /**
     * Create a new activity runner.
     *
     * @param spaceEnvironment
     *          space environment to use for this instance
     * @param liveActivityRunnerFactory
     *          live activity factory to use
     * @param nativeActivityRunnerFactory
     *          the factory for native activity runners
     */
    public StandaloneActivityRunner(InteractiveSpacesEnvironment spaceEnvironment,
            LiveActivityRunnerFactory liveActivityRunnerFactory,
            NativeActivityRunnerFactory nativeActivityRunnerFactory) {
        this.liveActivityRunnerFactory = liveActivityRunnerFactory;
        this.spaceEnvironment = spaceEnvironment;
        this.nativeActivityRunnerFactory = nativeActivityRunnerFactory;

        activityComponentFactory = new CoreExistingActivityComponentFactory();
    }

    /**
     * Get the instance suffix to use for this instance. The suffix returned depends on the number of already running
     * instances.
     *
     * @param rootDir
     *          the root directory that holds the instance locks
     *
     * @return suffix to use for directories and files
     */
    private String findInstanceSuffix(File rootDir) {
        if (instanceSuffix != null) {
            return instanceSuffix;
        }

        int instance = 0;
        while (instance < MAX_INSTANCE_COUNT) {
            String suffix = instance > 0 ? ("-" + instance) : "";
            File lockFile = new File(rootDir, LOCK_FILE_NAME + suffix);
            try {
                RandomAccessFile pidRaf = new RandomAccessFile(lockFile, "rw");
                FileLock fileLock = pidRaf.getChannel().tryLock(0, Long.MAX_VALUE, false);
                if (fileLock != null) {
                    return suffix;
                }
            } catch (IOException e) {
                // Do nothing, increment and try again.
            }
            instance++;
        }
        throw new InteractiveSpacesException("Could not lock run file after " + MAX_INSTANCE_COUNT + " tries");
    }

    /**
     * Get the configuration file to use for local activity configuration.
     *
     * @param configFile
     *          base configuration file
     *
     * @return file to use for local activity configuration
     */
    private File getInstanceConfigFile(File configFile) {
        String rootPath = configFile.getPath();
        int breakIndex = rootPath.lastIndexOf('.');
        String instanceName = rootPath.substring(0, breakIndex) + instanceSuffix + rootPath.substring(breakIndex);
        return new File(instanceName);
    }

    /**
     * Actually create the activity.
     */
    public void createActivity() {
        getLog().info("Creating activity " + activityRootDir.getAbsolutePath());
        LiveActivityConfiguration liveActivityConfiguration = getLiveActivityConfiguration();
        LiveActivityRunnerListener liveActivityRunnerListener = null;

        // TODO(keith): Change once have live activity runtime integration.
        // activeActivity =
        // liveActivityRunnerFactory.newLiveActivityRunner(getLiveActivity(), activityFilesystem,
        // liveActivityConfiguration, liveActivityRunnerListener, new BaseActivityRuntime(nativeActivityRunnerFactory,
        // activityComponentFactory, spaceEnvironment));
    }

    /**
     * Get a live activity configuration.
     *
     * @return live activity configuration
     */
    private LiveActivityConfiguration getLiveActivityConfiguration() {
        ExpressionEvaluator expressionEvaluator = new SimpleExpressionEvaluator();

        Configuration systemConfiguration = spaceEnvironment.getSystemConfiguration();
        addDynamicConfiguration(systemConfiguration);

        SingleConfigurationStorageManager baseConfigurationManager = new SimplePropertyFileSingleConfigurationStorageManager(
                true, activityFilesystem.getInstallFile(ACTIVITY_CONFIG_FILE_NAME), expressionEvaluator);
        baseConfigurationManager.load();

        SingleConfigurationStorageManager installedConfigurationManager = new SimplePropertyFileSingleConfigurationStorageManager(
                false, activityConfigFile, expressionEvaluator);
        installedConfigurationManager.load();

        SingleConfigurationStorageManager localConfigurationManager = new SimplePropertyFileSingleConfigurationStorageManager(
                false, localConfigFile, expressionEvaluator);
        localConfigurationManager.load();
        Map<String, String> collapsedMap = localConfigurationManager.getConfiguration().getCollapsedMap();
        installedConfigurationManager.update(collapsedMap);

        LiveActivityConfiguration configuration = new StandardLiveActivityConfiguration(baseConfigurationManager,
                installedConfigurationManager, expressionEvaluator, systemConfiguration);
        expressionEvaluator.setEvaluationEnvironment(configuration);
        return configuration;
    }

    /**
     * Add dynamic configuration parameters to this configuration.
     *
     * @param configuration
     *          the configuration to dynamically update
     */
    private void addDynamicConfiguration(Configuration configuration) {
        String platformOs = SystemUtils.IS_OS_LINUX ? "linux" : "osx";
        configuration.setValue("interactivespaces.platform.os", platformOs);

        try {
            String hostname = InetAddress.getLocalHost().getHostName();
            configuration.setValue(InteractiveSpacesEnvironment.CONFIGURATION_HOSTNAME, hostname);
            configuration.setValue(InteractiveSpacesEnvironment.CONFIGURATION_HOSTID, hostname);
            String hostAddress = InetAddress.getByName(hostname).getHostAddress();
            configuration.setValue(InteractiveSpacesEnvironment.CONFIGURATION_HOST_ADDRESS, hostAddress);
        } catch (UnknownHostException e) {
            throw new InteractiveSpacesException("Could not determine hostname", e);
        }
    }

    /**
     * Make the simple controller instance.
     */
    public void makeController() {
        if (controller != null) {
            throw new InteractiveSpacesException("controller already defined");
        }
        activityFilesystem = new SimpleLiveActivityFilesystem(activityRootDir);

        SimpleNativeActivityRunnerFactory simpleNativeActivityRunnerFactory = new SimpleNativeActivityRunnerFactory(
                spaceEnvironment);

        controller = new StandaloneSpaceController(spaceEnvironment, simpleNativeActivityRunnerFactory);

        controller.startup();

        if (useStandaloneRouter) {
            cecRouter = new StandaloneMessageRouter(this);
            controller.getSpaceEnvironment().setValue(MessageRouterActivityComponent.class.getName(), cecRouter);
        }
    }

    /**
     * Get a live activity instance.
     *
     * @return installed live activity instance
     */
    private InstalledLiveActivity getLiveActivity() {
        final InstalledLiveActivity liveActivity = new SimpleInstalledLiveActivity();

        Date installedDate = new Date(controller.getSpaceEnvironment().getTimeProvider().getCurrentTime());

        File activityFile = activityFilesystem.getInstallFile("activity.xml");

        InputStream activityDescriptionStream = null;
        Version version;
        String identifyingName;
        try {
            activityDescriptionStream = new FileInputStream(activityFile);
            ActivityDescriptionReader reader = new JdomActivityDescriptionReader();
            ActivityDescription activityDescription = reader.readDescription(activityDescriptionStream);
            version = Version.parseVersion(activityDescription.getVersion());
            identifyingName = activityDescription.getIdentifyingName();
        } catch (Exception e) {
            throw new InteractiveSpacesException(
                    "Could not read activity file description from " + activityFile.getAbsolutePath(), e);
        } finally {
            Closeables.closeQuietly(activityDescriptionStream);
        }

        liveActivity.setUuid(Long.toHexString((long) (Math.random() * RANDOM_UUID_MAX)));
        liveActivity.setBaseInstallationLocation(activityFilesystem.getInstallDirectory().getAbsolutePath());
        liveActivity.setIdentifyingName(identifyingName);
        liveActivity.setVersion(version);
        liveActivity.setLastDeployedDate(installedDate);
        liveActivity.setLastActivityState(ActivityState.READY);
        liveActivity.setInstallationStatus(ActivityInstallationStatus.OK);
        liveActivity.setRuntimeStartupType(ActivityRuntimeStartupType.READY);

        return liveActivity;
    }

    /**
     * Report the current status of the activity.
     */
    public void reportStatus() {
        getLog().info("Activity status: " + activeActivity.getCachedActivityStatus());
    }

    /**
     * Set the instance suffix for this instance.
     *
     * @param instanceSuffix
     *          instance suffix to use
     */
    public void setInstanceSuffix(String instanceSuffix) {
        this.instanceSuffix = instanceSuffix;
    }

    /**
     * Controls the router to use.
     *
     * @param useStandaloneRouter
     *          {@code true} if the standalone router should be used
     */
    public void setUseStandaloneRouter(boolean useStandaloneRouter) {
        this.useStandaloneRouter = useStandaloneRouter;
    }

    /**
     * Set the activity runtime dir.
     *
     * @param activityRuntimeDir
     *          directory to use for activity runtime files
     */
    public void setActivityRuntimeDir(File activityRuntimeDir) {
        instanceSuffix = findInstanceSuffix(activityRuntimeDir);
        isPrimaryInstance = instanceSuffix.length() == 0;
        File actualRootDir = isPrimaryInstance ? activityRuntimeDir
                : new File(activityRuntimeDir, "instance" + instanceSuffix);
        FileSupportImpl.INSTANCE.directoryExists(actualRootDir);
        this.activityRootDir = actualRootDir;
    }

    /**
     * Set the trace send path.
     *
     * @param traceSendPath
     *          trace send path
     */
    public void setTraceSendPath(String traceSendPath) {
        this.traceSendPath = traceSendPath;
    }

    /**
     * Set the local config file.
     *
     * @param localConfigFile
     *          local config file
     */
    public void setLocalConfigFile(File localConfigFile) {
        this.localConfigFile = getInstanceConfigFile(localConfigFile);
    }

    /**
     * Set the activity config file.
     *
     * @param activityConfigFile
     *          activity config file
     */
    public void setActivityConfigFile(File activityConfigFile) {
        this.activityConfigFile = getInstanceConfigFile(activityConfigFile);
    }

    /**
     * Set the trace check path.
     *
     * @param traceCheckPath
     *          trace check path
     */
    public void setTraceCheckPath(String traceCheckPath) {
        this.traceCheckPath = traceCheckPath;
    }

    /**
     * Set the trace playback path.
     *
     * @param tracePlaybackPath
     *          trace playback path
     */
    public void setTracePlaybackPath(String tracePlaybackPath) {
        this.tracePlaybackPath = tracePlaybackPath;
    }

    /**
     * Set the trace filter path.
     *
     * @param traceFilterPath
     *          trace filter path
     */
    public void setTraceFilterPath(String traceFilterPath) {
        this.traceFilterPath = traceFilterPath;
    }

    /**
     * Prepare the filesystem for use. Makes sure necessary directories exist.
     */
    public void prepareFilesystem() {
        fileSupport.directoryExists(new File(activityRootDir, "tmp"));
        fileSupport.directoryExists(new File(activityRootDir, "log"));

        if (!isPrimaryInstance) {
            // No standard way to do this until Java 7 -- so hack for linux.
            // TODO(peringknife): Move this to the activity runner wrapper, since it now knows about the instance.
            File targetInstallLink = new File(activityRootDir, "install");
            try {
                String command = "/bin/ln -sf ../install " + targetInstallLink.getAbsolutePath();
                spaceEnvironment.getLog().info("Executing: " + command);
                Runtime.getRuntime().exec(command);
            } catch (IOException e) {
                throw new InteractiveSpacesException("Could not execute link command", e);
            }

            // When running multiple instances, require explicit local config files to
            // make it more obvious what they should be.
            if (!activityConfigFile.exists()) {
                throw new InteractiveSpacesException(
                        "Missing activity config file " + activityConfigFile.getAbsolutePath());
            }
        }

        spaceEnvironment.getLog().info("Using activity config file " + activityConfigFile.getAbsolutePath());
    }

    /**
     * Start any trace playback.
     */
    public void startPlayback() {
        if (tracePlaybackPath != null) {
            cecRouter.playback(tracePlaybackPath, false);
        }

        if (traceSendPath != null) {
            cecRouter.playback(traceSendPath, true);
        }
    }

    /**
     * Startup the activity.
     */
    public void startupActivity() {
        checkActivityState(ActivityState.READY);

        if (traceCheckPath != null) {
            cecRouter.checkStart(traceCheckPath);
        }

        if (traceFilterPath != null) {
            cecRouter.setTraceFilter(traceFilterPath);
        }

        activeActivity.startup();
    }

    /**
     * Activate the activity.
     */
    public void activateActivity() {
        checkActivityState(ActivityState.RUNNING);
        activeActivity.activate();
    }

    /**
     * Check that the activity state is as expected.
     *
     * @param expectedState
     *          the expected state
     */
    private void checkActivityState(ActivityState expectedState) {
        ActivityState state = activeActivity.sampleActivityStatus().getState();
        if (!expectedState.equals(state)) {
            throw new InteractiveSpacesException(
                    String.format("Activity in improper state, was %s expecting %s", state, expectedState));
        }
    }

    /**
     * Signal completion, either due to an error or successful verification of all messages.
     *
     * @param success
     *          {@code true} if completion is due to successful message verification
     */
    public void signalCompletion(boolean success) {
        // Need to do this in another thread because we don't know the context the error is occurring in.
        // Specifically, the web-server gets unhappy if you try to exit from an io-worker thread.
        new Thread(new ExitHelper(success)).start();
    }

    /**
     * Handle an error by the activity.
     *
     * @param msg
     *          message to include with the error
     * @param throwable
     *          exception that caused the error
     */
    public synchronized void handleError(String msg, Throwable throwable) {
        getLog().error(msg, throwable);
    }

    /**
     * Helper runner class for making a clean exit.
     */
    class ExitHelper implements Runnable {
        /**
         * Save the success code, we need it when exiting.
         */
        private boolean success;

        /**
         * Exit helper.
         *
         * @param success
         *          success state of the system
         */
        public ExitHelper(boolean success) {
            this.success = success;
        }

        /**
         * Actually shut down the system.
         */
        @Override
        public void run() {
            try {
                activeActivity.shutdown();
            } catch (Exception e) {
                getLog().error("Error encountered during shutdown", e);
                success = false;
            }

            int returnCode = success ? 0 : 1;
            getLog().error("Exiting with result code " + returnCode);
            System.exit(returnCode);
        }
    }

    /**
     * @return logger to use
     */
    private Log getLog() {
        return controller.getSpaceEnvironment().getLog();
    }
}