/*
* Copyright (C) 2006-2016 DLR, Germany
*
* All rights reserved
*
* http://www.rcenvironment.de/
*/
package de.rcenvironment.core.remoteaccess.server.internal;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.QuoteMode;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import de.rcenvironment.core.communication.api.PlatformService;
import de.rcenvironment.core.communication.common.LogicalNodeId;
import de.rcenvironment.core.communication.common.NodeIdentifierUtils;
import de.rcenvironment.core.component.api.DistributedComponentKnowledge;
import de.rcenvironment.core.component.api.DistributedComponentKnowledgeService;
import de.rcenvironment.core.component.execution.api.SingleConsoleRowsProcessor;
import de.rcenvironment.core.component.model.api.ComponentDescription;
import de.rcenvironment.core.component.model.api.ComponentInstallation;
import de.rcenvironment.core.component.model.api.ComponentInterface;
import de.rcenvironment.core.component.model.endpoint.api.EndpointDefinition;
import de.rcenvironment.core.component.model.endpoint.api.EndpointDescription;
import de.rcenvironment.core.component.workflow.api.WorkflowConstants;
import de.rcenvironment.core.component.workflow.execution.api.FinalWorkflowState;
import de.rcenvironment.core.component.workflow.execution.api.WorkflowExecutionException;
import de.rcenvironment.core.component.workflow.execution.api.WorkflowFileException;
import de.rcenvironment.core.component.workflow.execution.headless.api.HeadlessWorkflowDescriptionLoaderCallback;
import de.rcenvironment.core.component.workflow.execution.headless.api.HeadlessWorkflowExecutionContextBuilder;
import de.rcenvironment.core.component.workflow.execution.headless.api.HeadlessWorkflowExecutionService;
import de.rcenvironment.core.component.workflow.model.api.WorkflowDescription;
import de.rcenvironment.core.component.workflow.model.api.WorkflowNode;
import de.rcenvironment.core.configuration.ConfigurationService;
import de.rcenvironment.core.configuration.ConfigurationService.ConfigurablePathId;
import de.rcenvironment.core.datamodel.api.DataType;
import de.rcenvironment.core.embedded.ssh.api.EmbeddedSshServerControl;
import de.rcenvironment.core.monitoring.system.api.LocalSystemMonitoringAggregationService;
import de.rcenvironment.core.monitoring.system.api.model.AverageOfDoubles;
import de.rcenvironment.core.monitoring.system.api.model.SystemLoadInformation;
import de.rcenvironment.core.remoteaccess.common.RemoteAccessConstants;
import de.rcenvironment.core.utils.common.InvalidFilenameException;
import de.rcenvironment.core.utils.common.StringUtils;
import de.rcenvironment.core.utils.common.TempFileService;
import de.rcenvironment.core.utils.common.TempFileServiceAccess;
import de.rcenvironment.core.utils.common.textstream.TextOutputReceiver;
import de.rcenvironment.core.utils.common.textstream.receivers.CapturingTextOutReceiver;
/**
* Provides "remote access" operations. TODO outline RA concept
*
* @author Robert Mischke
*/
// TODO @7.0.0: remove duplicate javadoc
// TODO @7.0.0: use better exception class than WorkflowExecutionException
public class RemoteAccessServiceImpl implements RemoteAccessService {
private static final int PERCENT_MULTIPLIER = 100;
private static final int INT_NO_DATA_PLACEHOLDER = -1;
private static final double DOUBLE_NO_DATA_PLACEHOLDER = -1.0;
private static final String INTERFACE_ENDPOINT_NAME_INPUT = "input";
private static final String INTERFACE_ENDPOINT_NAME_PARAMETERS = "parameters";
private static final String INTERFACE_ENDPOINT_NAME_OUTPUT = "output";
private static final String WF_PLACEHOLDER_PARAMETERS = "##RUNTIME_PARAMETERS##";
private static final String WF_PLACEHOLDER_INPUT_DIR = "##RUNTIME_INPUT_DIRECTORY##";
private static final String WF_PLACEHOLDER_OUTPUT_PARENT_DIR = "##RUNTIME_OUTPUT_DIRECTORY##";
private static final String WF_PLACEHOLDER_OUTPUT_FILES_FOLDER_NAME = "##OUTPUT_FILES_FOLDER_NAME##";
private static final String WORKFLOW_TEMPLATE_RESOURCE_PATH = "/resources/template.wf";
private static final String WF_PLACEHOLDER_TOOL_ID = "##TOOL_ID##";
private static final String WF_PLACEHOLDER_TOOL_VERSION = "##TOOL_VERSION##";
private static final String WF_PLACEHOLDER_TOOL_NODE_ID = "##TOOL_NODE_ID##";
private static final String WF_PLACEHOLDER_TIMESTAMP = "##TIMESTAMP##";
private static final String WORKFLOW_FILE_ENCODING = "UTF-8";
private static final String PUBLISHED_WF_DATA_FILE_SUFFIX = ".wf.dat";
private static final String PUBLISHED_WF_PLACEHOLDER_FILE_SUFFIX = ".ph.dat";
private static final String OUTPUT_INDENT = " ";
private final Log log = LogFactory.getLog(getClass());
private final Map<String, String> publishedWorkflowTemplates = new HashMap<>();
private final Map<String, String> publishedWorkflowTemplatePlaceholders = new HashMap<>();
private final TempFileService tempFileService = TempFileServiceAccess.getInstance();
private DistributedComponentKnowledgeService componentKnowledgeService;
private HeadlessWorkflowExecutionService workflowExecutionService;
private PlatformService platformService;
private ConfigurationService configurationService;
private LocalSystemMonitoringAggregationService localSystemMonitoringAggregationService;
private File publishedWfStorageDir;
private EmbeddedSshServerControl embeddedSshServerControl;
/**
* Simple holder for execution parameters, including the workflow template file.
*
* @author Robert Mischke
*/
private static final class ExecutionSetup {
private File workflowFile;
private File placeholdersFile;
private File inputFilesDir;
private File outputFilesDir;
ExecutionSetup(File wfFile, File placeholdersFile, File inputFilesDir, File outputFilesDir) {
this.workflowFile = wfFile;
this.placeholdersFile = placeholdersFile;
this.inputFilesDir = inputFilesDir;
this.outputFilesDir = outputFilesDir;
}
public File getWorkflowFile() {
return workflowFile;
}
public File getPlaceholderFile() {
return placeholdersFile;
}
public File getInputFilesDir() {
return inputFilesDir;
}
public File getOutputFilesDir() {
return outputFilesDir;
}
}
/**
* Simple boolean holder with "yes/no" formatting.
*
* @author Robert Mischke
*/
private static final class MutableYesNoFlag {
private boolean value;
public boolean getValue() {
return value;
}
public void setValue(boolean value) {
this.value = value;
}
@Override
public String toString() {
if (value) {
return "yes";
} else {
return "no";
}
}
}
/**
* OSGi life-cycle method.
*/
public void activate() {
initAndRestoreFromPublishedWfStorage();
embeddedSshServerControl.setAnnouncedVersionOrProperty("RemoteAccess", RemoteAccessConstants.PROTOCOL_VERSION_STRING);
}
@Override
public void printListOfAvailableTools(TextOutputReceiver outputReceiver, String format, boolean includeLoadData,
int timeSpanMsec, int timeLimitMsec) throws InterruptedException, ExecutionException, TimeoutException {
List<ComponentInstallation> components = getMatchingPublishedTools();
final Map<LogicalNodeId, SystemLoadInformation> systemLoadData;
if (includeLoadData) {
final Set<LogicalNodeId> reachableInstanceNodes = new HashSet<>();
for (ComponentInstallation c : components) {
reachableInstanceNodes.add(c.fetchNodeIdAsObject());
}
systemLoadData = localSystemMonitoringAggregationService
.collectSystemMonitoringDataWithTimeLimit(reachableInstanceNodes, timeSpanMsec, timeLimitMsec);
} else {
systemLoadData = null;
}
if ("csv".equals(format)) {
printComponentsListAsCsv(components, outputReceiver, systemLoadData);
} else if ("token-stream".equals(format)) {
printComponentsListAsTokens(components, outputReceiver, systemLoadData);
} else {
throw new IllegalArgumentException("Unrecognized output format: " + format);
}
}
@Override
public void printListOfAvailableWorkflows(TextOutputReceiver outputReceiver, String format) {
if (!"token-stream".equals(format)) {
throw new IllegalArgumentException("Unrecognized output format: " + format);
}
SortedSet<String> wfIds = new TreeSet<String>(publishedWorkflowTemplates.keySet());
outputReceiver.addOutput(Integer.toString(wfIds.size())); // number of entries
outputReceiver.addOutput("4"); // number of tokens per entry
for (String publishId : wfIds) {
// NOTE: the output format is made to match printListOfAvailableTools(); most fields are not used yet
if (!checkIdOrVersionString(publishId)) {
// for backwards compatility
log.error("Not listing the previously published remote access workflow " + publishId
+ "; the name contains characters that are not allowed anymore");
continue;
}
String nodeId = platformService.getLocalDefaultLogicalNodeId().getLogicalNodeIdString(); // changed in 8.0: showing LNIds
String nodeName = platformService.getLocalInstanceNodeSessionId().getAssociatedDisplayName();
outputReceiver.addOutput(publishId);
// TODO apply filtering too when versions are added
outputReceiver.addOutput("1"); // version; hardcoded for now
outputReceiver.addOutput(nodeId); // node id
outputReceiver.addOutput(nodeName); // node name
}
}
/**
* Creates a workflow file from an internal template and the given parameters, and executes it.
*
* @param toolId the id of the integrated tool to run (see CommonToolIntegratorComponent)
* @param toolVersion the version of the integrated tool to run
* @param toolNodeId the node id of the instance to run the tool on; must NOT be null (resolve+validate this first)
* @param parameterString an optional string containing tool-specific parameters
* @param inputFilesDir the local file system path to read input files from
* @param outputFilesDir the local file system path to write output files to
* @param consoleRowReceiver an optional listener for all received ConsoleRows; pass null to deactivate
* @return the state the generated workflow finished in
* @throws IOException on I/O errors
* @throws WorkflowExecutionException on workflow execution errors
*/
@Override
public FinalWorkflowState runSingleToolWorkflow(String toolId, String toolVersion, String toolNodeId, String parameterString,
File inputFilesDir, File outputFilesDir, SingleConsoleRowsProcessor consoleRowReceiver) throws IOException,
WorkflowExecutionException {
validateIdOrVersionString(toolId);
validateIdOrVersionString(toolVersion);
ExecutionSetup executionSetup =
generateSingleToolExecutionSetup(toolId, toolVersion, toolNodeId, parameterString, inputFilesDir, outputFilesDir);
return executeConfiguredWorkflow(executionSetup, consoleRowReceiver);
}
/**
* Executes a previously published workflow template.
*
* @param workflowId the id of the published workflow template
* @param parameterString an optional string containing tool-specific parameters
* @param inputFilesDir the local file system path to read input files from
* @param outputFilesDir the local file system path to write output files to
* @param consoleRowReceiver an optional listener for all received ConsoleRows; pass null to deactivate
* @return the state the generated workflow finished in
* @throws IOException on I/O errors
* @throws WorkflowExecutionException on workflow execution errors
*/
@Override
public FinalWorkflowState runPublishedWorkflowTemplate(String workflowId, String parameterString, File inputFilesDir,
File outputFilesDir, SingleConsoleRowsProcessor consoleRowReceiver) throws IOException, WorkflowExecutionException {
validateIdOrVersionString(workflowId);
// TODO validate version once added
ExecutionSetup executionSetup =
generateWorkflowExecutionSetup(workflowId, parameterString, inputFilesDir, outputFilesDir);
return executeConfiguredWorkflow(executionSetup, consoleRowReceiver);
}
/**
* Checks if the given workflow file can be used with the "wf-run" console command, and if this check is positive, the workflow file is
* published under the given id.
*
* @param wfFile the workflow file
* @param placeholdersFile TODO
* @param publishId the id by which the workflow file should be made available
* @param outputReceiver receiver for user feedback
* @param persistent make the publishing persistent
* @throws WorkflowExecutionException on failure to load/parse the workflow file
*/
@Override
public void checkAndPublishWorkflowFile(File wfFile, File placeholdersFile, String publishId, TextOutputReceiver outputReceiver,
boolean persistent) throws WorkflowExecutionException {
validateIdOrVersionString(publishId);
WorkflowDescription wd;
try {
wd = workflowExecutionService.loadWorkflowDescriptionFromFileConsideringUpdates(wfFile,
new HeadlessWorkflowDescriptionLoaderCallback(outputReceiver));
} catch (WorkflowFileException e) { // review migration code, which was introduced due to changed exception type
throw new WorkflowExecutionException("Failed to load workflow file: " + wfFile.getAbsolutePath(), e);
}
if (placeholdersFile != null) {
try {
workflowExecutionService.validatePlaceholdersFile(placeholdersFile);
} catch (WorkflowFileException e) { // review migration code, which was introduced due to changed exception type
throw new WorkflowExecutionException("Failed to validate placeholders file: " + wfFile.getAbsolutePath(), e);
}
}
File workflowStorageFile = getWorkflowStorageFile(publishId);
File placeholderStorageFile = getPlaceholderStorageFile(publishId);
// sanity check / user accident prevention
if (!persistent && workflowStorageFile.exists()) {
throw new WorkflowExecutionException(
"You are trying to overwrite a persistently published workflow with a temporary/transient one; "
+ "if this is what you want to do, unpublish the old workflow first, then publish the new one again");
}
outputReceiver.addOutput(StringUtils.format("Checking workflow file \"%s\"", wfFile.getAbsolutePath()));
if (validateWorkflowFileAsTemplate(wd, outputReceiver)) {
try {
String wfFileContent = readFile(wfFile);
String replaced = publishedWorkflowTemplates.put(publishId, wfFileContent);
// store wf file if told to persist
if (persistent) {
FileUtils.writeStringToFile(workflowStorageFile, wfFileContent);
}
if (placeholdersFile != null) {
String placeholdersFileContent = readFile(placeholdersFile);
publishedWorkflowTemplatePlaceholders.put(publishId, placeholdersFileContent);
// store placeholder file if told to persist
if (persistent) {
FileUtils.writeStringToFile(placeholderStorageFile, placeholdersFileContent);
}
} else {
// remove any pre-existing placeholder file's content
publishedWorkflowTemplatePlaceholders.put(publishId, null);
}
if (replaced == null) {
outputReceiver.addOutput(StringUtils.format("Successfully published workflow \"%s\"", publishId));
} else {
outputReceiver.addOutput(StringUtils.format("Successfully updated the published workflow \"%s\"", publishId));
}
} catch (IOException e) {
// avoid dangling, undefined workflow files on failure
publishedWorkflowTemplates.remove(publishId);
FileUtils.deleteQuietly(workflowStorageFile);
throw new WorkflowExecutionException("Error publishing workflow file " + wfFile.getAbsolutePath());
}
}
}
@Override
public void unpublishWorkflowForId(String publishId, TextOutputReceiver outputReceiver) throws WorkflowExecutionException {
validateIdOrVersionString(publishId);
String removed = publishedWorkflowTemplates.remove(publishId);
publishedWorkflowTemplatePlaceholders.remove(publishId);
// always try to delete the storage files; if publishing was temporary, they are simply not found
File workflowStorageFile = getWorkflowStorageFile(publishId);
if (workflowStorageFile.isFile()) {
try {
Files.delete(workflowStorageFile.toPath());
} catch (IOException e) {
throw new WorkflowExecutionException("Failed to unpublish the specified workflow; its storage file may be write-protected");
}
}
File placeholderStorageFile = getPlaceholderStorageFile(publishId);
if (placeholderStorageFile.isFile()) {
try {
Files.delete(placeholderStorageFile.toPath());
} catch (IOException e) {
throw new WorkflowExecutionException("Failed to unpublish the published placeholder file "
+ "for the specified workflow; its storage file may be write-protected");
}
}
if (removed != null) {
outputReceiver.addOutput(StringUtils.format("Successfully unpublished workflow \"%s\"", publishId));
} else {
outputReceiver.addOutput(StringUtils.format("ERROR: There is no workflow with id \"%s\" to unpublish", publishId));
}
}
/**
* Prints human-readable information about all published workflows.
*
* @param outputReceiver the receiver for the generated output
*/
@Override
public void printSummaryOfPublishedWorkflows(TextOutputReceiver outputReceiver) {
if (publishedWorkflowTemplates.isEmpty()) {
outputReceiver.addOutput("There are no workflows published for remote execution");
return;
}
outputReceiver.addOutput("Workflows published for remote execution:");
for (String publishId : publishedWorkflowTemplates.keySet()) {
String placeholders = "no";
if (publishedWorkflowTemplatePlaceholders.get(publishId) != null) {
placeholders = "yes";
}
outputReceiver.addOutput(StringUtils.format("- %s (using placeholders: %s)", publishId, placeholders));
}
}
@Override
// TODO add unit test
public String validateToolParametersAndGetFinalNodeId(String toolId, String toolVersion, String nodeId)
throws WorkflowExecutionException {
List<ComponentInstallation> availableTools = getMatchingPublishedTools();
// note: not strictly necessary, but gives more consistent error messages instead of "tool not found"
validateIdOrVersionString(toolId);
validateIdOrVersionString(toolVersion);
// only needed for nodeId == null to detect ambiguous matches
ComponentInstallation nodeMatch = null;
// TODO once components are cached, optimize with map lookup
for (ComponentInstallation compInst : availableTools) {
ComponentInterface compInterface = compInst.getComponentRevision().getComponentInterface();
// TODO (p2) "display name" sounds odd here, but seems to be the public id; check
if (toolId.equals(compInterface.getDisplayName())) {
if (toolVersion.equals(compInterface.getVersion())) {
if (nodeId != null) {
// specific node id: exit on first match
if (nodeId.equals(compInst.getNodeId())) {
return compInst.getNodeId();
}
} else {
if (nodeMatch == null) {
nodeMatch = compInst;
} else {
throw new WorkflowExecutionException(StringUtils.format("Tool selection is ambiguous without a node id; "
+ "tool '%s', version '%s' is provided by more than one node", toolId, toolVersion));
}
}
}
}
}
if (nodeId == null) {
if (nodeMatch != null) {
// success; single node match
return nodeMatch.getNodeId();
} else {
throw new WorkflowExecutionException(StringUtils.format("No matching tool for tool '%s' in version '%s'", toolId,
toolVersion, nodeId));
}
} else {
throw new WorkflowExecutionException(StringUtils.format("No matching tool for tool '%s' in version '%s', "
+ "running on a node with id '%s'", toolId, toolVersion, nodeId));
}
}
/**
* OSGi-DS bind method.
*
* @param newInstance the new service instance
*/
public void bindWorkflowExecutionService(HeadlessWorkflowExecutionService newInstance) {
this.workflowExecutionService = newInstance;
}
/**
* OSGi-DS bind method.
*
* @param newInstance the new service instance
*/
public void bindPlatformService(PlatformService newInstance) {
this.platformService = newInstance;
}
/**
* OSGi-DS bind method.
*
* @param newInstance the new service instance
*/
public void bindDistributedComponentKnowledgeService(DistributedComponentKnowledgeService newInstance) {
this.componentKnowledgeService = newInstance;
}
/**
* OSGi-DS bind method.
*
* @param newInstance the new service instance
*/
public void bindLocalSystemMonitoringAggregationService(LocalSystemMonitoringAggregationService newInstance) {
this.localSystemMonitoringAggregationService = newInstance;
}
/**
* OSGi-DS bind method.
*
* @param newInstance the new service instance
*/
public void bindEmbeddedSshServerControl(EmbeddedSshServerControl newInstance) {
this.embeddedSshServerControl = newInstance;
}
/**
* OSGi-DS bind method.
*
* @param newInstance the new service instance
*/
public void bindConfigurationService(ConfigurationService newInstance) {
this.configurationService = newInstance;
}
private void initAndRestoreFromPublishedWfStorage() {
// initialize the storage location for published workflows and placeholder data
publishedWfStorageDir =
new File(configurationService.getConfigurablePath(ConfigurablePathId.PROFILE_INTERNAL_DATA), "ra/published-wf");
publishedWfStorageDir.mkdirs();
if (!publishedWfStorageDir.isDirectory()) {
log.error("Failed to create Remote Access workflow storage directory " + publishedWfStorageDir.getAbsolutePath());
publishedWfStorageDir = null;
return;
}
// restore persisted data
for (File f : publishedWfStorageDir.listFiles()) {
String filename = f.getName();
if (filename.endsWith(PUBLISHED_WF_DATA_FILE_SUFFIX)) {
String wfId = filename.substring(0, filename.length() - PUBLISHED_WF_DATA_FILE_SUFFIX.length());
try {
publishedWorkflowTemplates.put(wfId, FileUtils.readFileToString(f));
} catch (IOException e) {
log.error("Failed to restore data of published RemoteAccess workflow from storage file " + f.getAbsolutePath(), e);
}
} else if (filename.endsWith(PUBLISHED_WF_PLACEHOLDER_FILE_SUFFIX)) {
String wfId = filename.substring(0, filename.length() - PUBLISHED_WF_PLACEHOLDER_FILE_SUFFIX.length());
try {
publishedWorkflowTemplatePlaceholders.put(wfId, FileUtils.readFileToString(f));
} catch (IOException e) {
log.error("Failed to restore placeholder data of published RemoteAccess workflow "
+ "from storage file " + f.getAbsolutePath(), e);
}
} else {
log.error("Unexpected file in RemoteAccess storage directory, ignoring: " + f.getAbsolutePath());
}
}
// TODO check for placeholder data without a workflow file? only sanity check; no actual harm in them - misc_ro
}
private File getWorkflowStorageFile(String id) throws WorkflowExecutionException {
if (publishedWfStorageDir == null) {
throw new WorkflowExecutionException(
"The workflow storage directory was not properly initialized; cannot execute this command");
}
File file = new File(publishedWfStorageDir, id + PUBLISHED_WF_DATA_FILE_SUFFIX);
log.debug("Resolved workflow publish id to storage filename " + file.getAbsolutePath());
return file;
}
private File getPlaceholderStorageFile(String id) throws WorkflowExecutionException {
if (publishedWfStorageDir == null) {
throw new WorkflowExecutionException(
"The workflow storage directory was not properly initialized; cannot execute this command");
}
return new File(publishedWfStorageDir, id + PUBLISHED_WF_PLACEHOLDER_FILE_SUFFIX);
}
private boolean isComponentSuitableAsRemoteAccessTool(ComponentInstallation compInst) {
ComponentInterface compInterf = compInst.getComponentRevision().getComponentInterface();
EndpointDefinition endpoint;
// validate id and version
if (!checkIdOrVersionString(compInterf.getDisplayName()) || !checkIdOrVersionString(compInterf.getVersion())) {
return false;
}
// do not allow more that two static inputs, as this may block execution
if (compInterf.getInputDefinitionsProvider().getStaticEndpointDefinitions().size() != 2) {
return false;
}
endpoint = compInterf.getInputDefinitionsProvider().getStaticEndpointDefinition(INTERFACE_ENDPOINT_NAME_INPUT);
if (endpoint == null || !endpoint.getPossibleDataTypes().contains(DataType.DirectoryReference)) {
return false;
}
endpoint = compInterf.getInputDefinitionsProvider().getStaticEndpointDefinition(INTERFACE_ENDPOINT_NAME_PARAMETERS);
if (endpoint == null || !endpoint.getPossibleDataTypes().contains(DataType.ShortText)) {
return false;
}
// additional outputs are allowed for now
endpoint = compInterf.getOutputDefinitionsProvider().getStaticEndpointDefinition(INTERFACE_ENDPOINT_NAME_OUTPUT);
if (endpoint == null || endpoint.getDefaultDataType() != DataType.DirectoryReference) {
return false;
}
return true;
}
private List<ComponentInstallation> getMatchingPublishedTools() {
List<ComponentInstallation> components = new ArrayList<>();
DistributedComponentKnowledge compKnowledge = componentKnowledgeService.getCurrentComponentKnowledge();
for (ComponentInstallation ci : compKnowledge.getAllPublishedInstallations()) {
if (isComponentSuitableAsRemoteAccessTool(ci)) {
components.add(ci);
}
}
// TODO sort?
return components;
}
private void printComponentsListAsCsv(List<ComponentInstallation> components, TextOutputReceiver outputReceiver,
Map<LogicalNodeId, SystemLoadInformation> systemLoadData) {
SortedSet<String> lines = new TreeSet<>();
final CSVFormat csvFormat = CSVFormat.newFormat(' ').withQuote('"').withQuoteMode(QuoteMode.ALL);
for (ComponentInstallation ci : components) {
ComponentInterface compInterface = ci.getComponentRevision().getComponentInterface();
String nodeId = ci.getNodeId();
String nodeName = NodeIdentifierUtils.parseLogicalNodeIdStringWithExceptionWrapping(nodeId).getAssociatedDisplayName();
if (systemLoadData != null) {
final SystemLoadInformation loadDataEntry = systemLoadData.get(ci.fetchNodeIdAsObject());
// two-step checking as there may be no load data available for that node
final double cpuAvg;
final int numSamples;
final int timeSpan;
final long availableRam;
if (loadDataEntry != null) {
final AverageOfDoubles cpuLoadAvg = loadDataEntry.getCpuLoadAvg();
cpuAvg = cpuLoadAvg.getAverage() * PERCENT_MULTIPLIER;
numSamples = cpuLoadAvg.getNumSamples();
timeSpan = cpuLoadAvg.getNumSamples()
* LocalSystemMonitoringAggregationService.SYSTEM_LOAD_INFORMATION_COLLECTION_INTERVAL_MSEC;
availableRam = loadDataEntry.getAvailableRam();
} else {
cpuAvg = DOUBLE_NO_DATA_PLACEHOLDER;
numSamples = INT_NO_DATA_PLACEHOLDER;
timeSpan = INT_NO_DATA_PLACEHOLDER;
availableRam = INT_NO_DATA_PLACEHOLDER;
}
lines.add(csvFormat.format(compInterface.getDisplayName(), compInterface.getVersion(), nodeId, nodeName,
StringUtils.format("%.2f", cpuAvg), numSamples, timeSpan, availableRam));
} else {
lines.add(csvFormat.format(compInterface.getDisplayName(), compInterface.getVersion(), nodeId, nodeName));
}
}
for (String line : lines) {
outputReceiver.addOutput(line);
}
}
private void printComponentsListAsTokens(List<ComponentInstallation> components, TextOutputReceiver outputReceiver,
Map<LogicalNodeId, SystemLoadInformation> systemLoadData) {
outputReceiver.addOutput(Integer.toString(components.size())); // number of entries
// print number of tokens per entry
if (systemLoadData != null) {
outputReceiver.addOutput("8");
} else {
outputReceiver.addOutput("4");
}
for (ComponentInstallation ci : components) {
ComponentInterface compInterface = ci.getComponentRevision().getComponentInterface();
String nodeId = ci.getNodeId();
String nodeName =
NodeIdentifierUtils.parseArbitraryIdStringToLogicalNodeIdWithExceptionWrapping(nodeId).getAssociatedDisplayName();
outputReceiver.addOutput(compInterface.getDisplayName());
outputReceiver.addOutput(compInterface.getVersion());
outputReceiver.addOutput(nodeId);
outputReceiver.addOutput(nodeName);
if (systemLoadData != null) {
// TODO (p3) extract common code with above method?
final SystemLoadInformation loadDataEntry = systemLoadData.get(ci.fetchNodeIdAsObject());
// two-step checking as there may be no load data available for that node
final double cpuAvg;
final int numSamples;
final int timeSpan;
final long availableRam;
if (loadDataEntry != null) {
final AverageOfDoubles cpuLoadAvg = loadDataEntry.getCpuLoadAvg();
cpuAvg = cpuLoadAvg.getAverage() * PERCENT_MULTIPLIER;
numSamples = cpuLoadAvg.getNumSamples();
timeSpan = cpuLoadAvg.getNumSamples()
* LocalSystemMonitoringAggregationService.SYSTEM_LOAD_INFORMATION_COLLECTION_INTERVAL_MSEC;
availableRam = loadDataEntry.getAvailableRam();
} else {
cpuAvg = DOUBLE_NO_DATA_PLACEHOLDER;
numSamples = INT_NO_DATA_PLACEHOLDER;
timeSpan = INT_NO_DATA_PLACEHOLDER;
availableRam = INT_NO_DATA_PLACEHOLDER;
}
outputReceiver.addOutput(StringUtils.format("%.2f", cpuAvg));
outputReceiver.addOutput(Integer.toString(numSamples));
outputReceiver.addOutput(Integer.toString(timeSpan));
outputReceiver.addOutput(Long.toString(availableRam));
}
}
}
private boolean validateWorkflowFileAsTemplate(WorkflowDescription wd, TextOutputReceiver outputReceiver)
throws WorkflowExecutionException {
validateEquals(WorkflowConstants.CURRENT_WORKFLOW_VERSION_NUMBER, wd.getWorkflowVersion(), "Invalid workflow file version");
MutableYesNoFlag foundInputDirSource = new MutableYesNoFlag();
MutableYesNoFlag foundParametersSource = new MutableYesNoFlag();
MutableYesNoFlag foundOutputReceiver = new MutableYesNoFlag();
for (WorkflowNode node : wd.getWorkflowNodes()) {
outputReceiver.addOutput(OUTPUT_INDENT + "Checking component \"" + node.getName() + "\" [" + node.getIdentifier() + "]");
final ComponentDescription compDesc = node.getComponentDescription();
final String compId = compDesc.getIdentifier();
final String compVersion = compDesc.getVersion();
if (compId.startsWith("de.rcenvironment.inputprovider/")) {
validateEquals("3.2", compVersion, "Invalid component version");
for (EndpointDescription outputEndpoint : node.getOutputDescriptionsManager().getDynamicEndpointDescriptions()) {
checkEndpoint(outputEndpoint, "input directory source", INTERFACE_ENDPOINT_NAME_INPUT, DataType.DirectoryReference,
WF_PLACEHOLDER_INPUT_DIR,
foundInputDirSource, outputReceiver);
checkEndpoint(outputEndpoint, "input parameters source", INTERFACE_ENDPOINT_NAME_PARAMETERS, DataType.ShortText,
WF_PLACEHOLDER_PARAMETERS,
foundParametersSource, outputReceiver);
}
} else if (compId.startsWith("de.rcenvironment.outputwriter/")) {
validateEquals("2.0", compVersion, "Invalid component version");
Map<String, String> compConfig = compDesc.getConfigurationDescription().getConfiguration();
String selectedRoot = compConfig.get("SelectedRoot");
if (WF_PLACEHOLDER_OUTPUT_PARENT_DIR.equals(selectedRoot)) {
for (EndpointDescription inputEndpoint : node.getInputDescriptionsManager().getDynamicEndpointDescriptions()) {
checkEndpoint(inputEndpoint, "output directory receiver", INTERFACE_ENDPOINT_NAME_OUTPUT,
DataType.DirectoryReference, null,
foundOutputReceiver, outputReceiver);
}
validateEquals("false", compConfig.get("SelectRootOnWorkflowStart"), "Invalid \"Select at workflow start\" setting");
} else {
printEndpointValidationMessage(outputReceiver, StringUtils.format(
"Ignoring this Output Writer as its \"Root folder\" setting is not the \"%s\" marker",
WF_PLACEHOLDER_OUTPUT_PARENT_DIR));
}
}
}
// check for completeness
if (foundInputDirSource.getValue() && foundParametersSource.getValue() && foundOutputReceiver.getValue()) {
outputReceiver.addOutput("Validation successful");
return true;
} else {
outputReceiver.addOutput("Validation failed:");
outputReceiver.addOutput(OUTPUT_INDENT + "Found input directory source: " + foundInputDirSource);
outputReceiver.addOutput(OUTPUT_INDENT + "Found input parameters source: " + foundParametersSource);
outputReceiver.addOutput(OUTPUT_INDENT + "Found output receiver: " + foundOutputReceiver);
return false;
}
}
private void checkEndpoint(EndpointDescription endpoint, String description, String expectedName, DataType expectedDataType,
String placeholderMarker, MutableYesNoFlag detectionFlag, TextOutputReceiver outputReceiver) throws WorkflowExecutionException {
final String actualName = endpoint.getName();
final DataType actualDataType = endpoint.getDataType();
final boolean dataTypeMatches = expectedDataType == actualDataType;
final boolean nameMatches = expectedName.equals(actualName);
boolean allMatched = false;
// minor hack to satisfy the CheckStyle "<= 6 parameters" rule: derive "isInputSide" value from the fact if a placeholderMarker is
// set
if (placeholderMarker != null) {
final boolean hasMarkerValue = placeholderMarker.equals(endpoint.getMetaDataValue("startValue"));
if (!nameMatches && !hasMarkerValue) {
// neither name nor marker matches -> ignore silently
return;
}
if (nameMatches && dataTypeMatches && hasMarkerValue) {
allMatched = true;
} else {
printEndpointValidationMessage(outputReceiver, StringUtils.format(
"Output \"%s\" is a candidate for the %s, but it does not quite match: ", expectedName, description));
if (!nameMatches) {
printEndpointValidationMessage(outputReceiver,
StringUtils.format(" - Unexpected name \"%s\" instead of \"%s\"", actualName, expectedName));
}
if (!dataTypeMatches) {
printEndpointValidationMessage(outputReceiver,
StringUtils.format(" - Unexpected data type \"%s\" instead of \"%s\"", actualDataType.getDisplayName(),
expectedDataType.getDisplayName()));
}
if (!hasMarkerValue) {
printEndpointValidationMessage(outputReceiver,
StringUtils.format(" - Marker value \"%s\" not found", placeholderMarker));
}
return;
}
} else {
if (!nameMatches || !dataTypeMatches) {
printEndpointValidationMessage(outputReceiver, StringUtils.format(
"Input \"%s\" is a candidate for the %s, but it does not quite match: ", actualName, description));
if (!nameMatches) {
printEndpointValidationMessage(outputReceiver,
StringUtils.format(" - Unexpected name \"%s\" instead of \"%s\"", actualName, expectedName));
}
if (!dataTypeMatches) {
printEndpointValidationMessage(outputReceiver,
StringUtils.format(" - Unexpected data type \"%s\" instead of \"%s\"", actualDataType.getDisplayName(),
expectedDataType.getDisplayName()));
}
return;
}
// note: "placeholder" parameter is not used for output receiver
validateEquals("output", endpoint.getMetaDataValue("filename"),
"Invalid \"Target name\" setting in Output Writer; must be \"output\"");
validateEquals("[root]", endpoint.getMetaDataValue("folderForSaving"),
"Invalid \"Target folder\" setting in Output Writer; must be \"[root]\"");
allMatched = true;
}
// check against accidental fall-through
if (!allMatched) {
throw new IllegalStateException("Internal error: Expected flag not set");
}
// check for duplicate and set flag if not
if (detectionFlag.getValue()) {
throw new WorkflowExecutionException("Found more than one " + description + " provider");
} else {
printEndpointValidationMessage(outputReceiver, StringUtils.format("Found %s \"%s\"", description, actualName));
detectionFlag.setValue(true);
}
}
private void printEndpointValidationMessage(TextOutputReceiver outputReceiver, String message) {
outputReceiver.addOutput(OUTPUT_INDENT + OUTPUT_INDENT + message);
}
private void validateEquals(Object expected, Object actual, String message) throws WorkflowExecutionException {
if (!expected.equals(actual)) {
throw new WorkflowExecutionException(StringUtils.format("%s: Expected \"%s\", but found \"%s\"", message, expected, actual));
}
}
// returns boolean result
private boolean checkIdOrVersionString(String id) {
return StringUtils.checkAgainstCommonInputRules(id) == null;
}
// throws exception on failure
private void validateIdOrVersionString(String id) throws WorkflowExecutionException {
// TODO add integration for high-level commands using this
String valdationErrorMessage = StringUtils.checkAgainstCommonInputRules(id);
if (valdationErrorMessage != null) {
throw new WorkflowExecutionException("Invalid tool id, workflow id, or version \"" + id + "\": " + valdationErrorMessage);
}
}
private String readFile(File placeholdersFile) throws IOException {
return FileUtils.readFileToString(placeholdersFile, WORKFLOW_FILE_ENCODING);
}
private void renameAsOld(File outputFilesDir) {
File tempDestination =
new File(outputFilesDir.getParentFile(), outputFilesDir.getName() + ".old." + System.currentTimeMillis());
outputFilesDir.renameTo(tempDestination);
if (outputFilesDir.isDirectory()) {
log.warn("Tried to move directory " + outputFilesDir.getAbsolutePath() + " to "
+ tempDestination.getAbsolutePath()
+ ", but it is still present");
}
}
private ExecutionSetup generateSingleToolExecutionSetup(String toolId, String toolVersion, String toolNodeId, String parameterString,
File inputFilesDir, File outputFilesDir) throws IOException {
InputStream templateStream = getClass().getResourceAsStream(WORKFLOW_TEMPLATE_RESOURCE_PATH);
if (templateStream == null) {
throw new IOException("Failed to read remote tool access template");
}
String template = IOUtils.toString(templateStream, WORKFLOW_FILE_ENCODING);
if (template == null || template.isEmpty()) {
throw new IOException("Found remote tool access template, but had empty content after loading it");
}
final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss");
final String timestampString = dateFormat.format(new Date());
String workflowContent = template
.replace(WF_PLACEHOLDER_TOOL_ID, toolId)
.replace(WF_PLACEHOLDER_TOOL_VERSION, toolVersion) // guarded by validation; no escaping necessary
.replace(WF_PLACEHOLDER_TOOL_NODE_ID, toolNodeId) // guarded by validation; no escaping necessary
.replace(WF_PLACEHOLDER_PARAMETERS, StringUtils.escapeAsJsonStringContent(parameterString, false))
.replace(WF_PLACEHOLDER_TIMESTAMP, timestampString)
.replace(WF_PLACEHOLDER_INPUT_DIR, formatPathForWorkflowFile(inputFilesDir))
// note: the name splitting is needed due to OutputWriter constraints - misc_ro
// TODO (p1) >8.0.0 - recheck after placeholder change (?)
.replace(WF_PLACEHOLDER_OUTPUT_PARENT_DIR, formatPathForWorkflowFile(outputFilesDir.getParentFile()))
.replace(WF_PLACEHOLDER_OUTPUT_FILES_FOLDER_NAME, outputFilesDir.getName());
File wfFile = tempFileService.createTempFileFromPattern("rta-*.wf");
FileUtils.write(wfFile, workflowContent, WORKFLOW_FILE_ENCODING);
return new ExecutionSetup(wfFile, null, inputFilesDir, outputFilesDir);
}
private ExecutionSetup generateWorkflowExecutionSetup(String workflowId, String parameterString, File inputFilesDir,
File outputFilesDir) throws IOException, WorkflowExecutionException {
final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss");
final String timestampString = dateFormat.format(new Date());
final String template = publishedWorkflowTemplates.get(workflowId);
final String placeholdersFileContent = publishedWorkflowTemplatePlaceholders.get(workflowId);
if (template == null) {
throw new WorkflowExecutionException("There is no published workflow for id " + workflowId);
}
File placeholdersFile = null;
if (placeholdersFileContent != null) {
placeholdersFile = tempFileService.createTempFileFromPattern("rwa-properties-*.json");
FileUtils.writeStringToFile(placeholdersFile, placeholdersFileContent, WORKFLOW_FILE_ENCODING);
}
String workflowContent = template
.replace(WF_PLACEHOLDER_PARAMETERS, StringUtils.escapeAsJsonStringContent(parameterString, false)) // prevent injection
.replace(WF_PLACEHOLDER_TIMESTAMP, timestampString)
.replace(WF_PLACEHOLDER_INPUT_DIR, formatPathForWorkflowFile(inputFilesDir))
.replace(WF_PLACEHOLDER_OUTPUT_PARENT_DIR, formatPathForWorkflowFile(outputFilesDir.getParentFile()));
File wfFile = tempFileService.createTempFileFromPattern("rwa-*.wf");
FileUtils.write(wfFile, workflowContent, WORKFLOW_FILE_ENCODING);
return new ExecutionSetup(wfFile, placeholdersFile, inputFilesDir, outputFilesDir);
}
private FinalWorkflowState executeConfiguredWorkflow(ExecutionSetup executionSetup,
SingleConsoleRowsProcessor customConsoleRowReceiver) throws WorkflowExecutionException {
log.debug("Executing remote access workflow " + executionSetup.getWorkflowFile().getAbsolutePath());
File inputFilesDir = executionSetup.getInputFilesDir();
File outputFilesDir = executionSetup.getOutputFilesDir();
// move the output directory if it already exists to avoid collisions
if (outputFilesDir.isDirectory()) {
renameAsOld(outputFilesDir);
}
File logDir = new File(outputFilesDir.getParent(), "logs");
if (logDir.isDirectory()) {
renameAsOld(logDir);
}
logDir.mkdirs();
// TODO review >5.0.0: remove this output capture, as it is only used for debug output? - misc_ro
CapturingTextOutReceiver outputReceiver = new CapturingTextOutReceiver("");
// TODO specify log directory?
HeadlessWorkflowExecutionContextBuilder exeContextBuilder;
try {
exeContextBuilder = new HeadlessWorkflowExecutionContextBuilder(executionSetup.getWorkflowFile(), logDir);
} catch (InvalidFilenameException e) {
// This exception should never occur since the name of the workflow file used here is generated by the
// generateWorkflowExecutionSetup method and is always valid
throw new IllegalStateException();
}
exeContextBuilder.setPlaceholdersFile(executionSetup.getPlaceholderFile());
exeContextBuilder.setTextOutputReceiver(outputReceiver);
exeContextBuilder.setSingleConsoleRowsProcessor(customConsoleRowReceiver);
exeContextBuilder.setAbortIfWorkflowUpdateRequired(true); // fail on out-of-date templates
WorkflowExecutionException executionException = null;
FinalWorkflowState finalState = FinalWorkflowState.FAILED;
try {
finalState = workflowExecutionService.executeWorkflowSync(exeContextBuilder.build());
} catch (WorkflowExecutionException e) {
executionException = e;
File exceptionLogFile = new File(logDir, "error.log");
// create a log file so the error cause is accessible via the log directory
try {
FileUtils.writeStringToFile(exceptionLogFile, "Workflow execution failed with an error: " + e.toString());
} catch (IOException e1) {
log.error("Failed to write exception log file " + exceptionLogFile.getAbsolutePath());
}
}
log.debug("Finished remote access workflow; captured output:\n" + outputReceiver.getBufferedOutput());
// move the input directory to avoid future collisions
if (inputFilesDir.isDirectory()) {
File tempDestination = new File(inputFilesDir.getParentFile(), "input.old." + System.currentTimeMillis());
inputFilesDir.renameTo(tempDestination);
if (inputFilesDir.isDirectory()) {
log.warn("Tried to rename input directory " + inputFilesDir.getAbsolutePath() + " to " + tempDestination.getAbsolutePath()
+ ", but it is still present");
}
}
if (executionException != null) {
throw executionException;
}
return finalState;
}
private CharSequence formatPathForWorkflowFile(File directory) {
return directory.getAbsolutePath().replaceAll("\\\\", "/"); // double escaping for java+regexp; replaces "\"->"/"
}
}