/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.core.monitoring.system.api; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import de.rcenvironment.core.command.common.CommandException; import de.rcenvironment.core.command.spi.CommandContext; import de.rcenvironment.core.command.spi.CommandDescription; import de.rcenvironment.core.command.spi.CommandPlugin; import de.rcenvironment.core.communication.api.CommunicationService; import de.rcenvironment.core.communication.api.PlatformService; import de.rcenvironment.core.communication.common.InstanceNodeSessionId; import de.rcenvironment.core.monitoring.system.api.model.AverageOfDoubles; import de.rcenvironment.core.monitoring.system.api.model.SystemLoadInformation; import de.rcenvironment.core.utils.common.StringUtils; /** * A {@link CommandPlugin} providing console commands to fetch system monitoring and load data from the local and remote nodes. * * @author Robert Mischke */ public class SystemMonitoringCommandPlugin implements CommandPlugin { private static final String ROOT_COMMAND = "sysmon"; private static final String SUBCOMMAND_FETCH_LOCAL = "local"; private static final String SUBCOMMAND_FETCH_LOCAL_SHORT = "-l"; private static final String SUBCOMMAND_FETCH_REMOTE = "remote"; private static final String SUBCOMMAND_FETCH_REMOTE_SHORT = "-r"; private static final String SUBCOMMAND_API = "api"; private static final String SPACE = " "; private static final int DEFAULT_FETCH_TIME_SPAN_VALUE_SEC = 10; private static final int DEFAULT_FETCH_TIME_LIMIT_VALUE_MSEC = 1000; private static final int SEC_TO_MSEC = 1000; private LocalSystemMonitoringAggregationService localSystemMonitoringAggregationService; private CommunicationService communicationService; private InstanceNodeSessionId localInstanceNodeSessionId; @Override public void execute(final CommandContext context) throws CommandException { context.consumeExpectedToken(ROOT_COMMAND); final String subCommand = context.consumeNextToken(); if (subCommand == null) { // TODO (p2) improve once new command help/parsing system is in place throw CommandException.syntaxError("Missing operation argument (e.g. \"" + ROOT_COMMAND + " " + SUBCOMMAND_FETCH_REMOTE + "\")", context); } switch (subCommand) { case SUBCOMMAND_FETCH_LOCAL: case SUBCOMMAND_FETCH_LOCAL_SHORT: // arbitrary timespan for default fetch: 10 seconds performPrintLocalSysMonData(context, DEFAULT_FETCH_TIME_SPAN_VALUE_SEC * SEC_TO_MSEC, DEFAULT_FETCH_TIME_LIMIT_VALUE_MSEC, true); break; case SUBCOMMAND_FETCH_REMOTE: case SUBCOMMAND_FETCH_REMOTE_SHORT: // arbitrary timespan for default fetch: 10 seconds performCollectAndPrintSysMonData(context, DEFAULT_FETCH_TIME_SPAN_VALUE_SEC * SEC_TO_MSEC, DEFAULT_FETCH_TIME_LIMIT_VALUE_MSEC, true); break; case SUBCOMMAND_API: String apiCall = context.consumeNextToken(); if (apiCall == null) { throw CommandException.wrongNumberOfParameters(context); } switch (apiCall) { case "default": case "avgcpu+ram": final int timeSpanSec = parseRequiredPositiveIntParameter(context, "time span"); final int timeLimitMsec = parseRequiredPositiveIntParameter(context, "time limit"); performCollectAndPrintSysMonData(context, timeSpanSec * SEC_TO_MSEC, timeLimitMsec, false); break; default: throw CommandException.syntaxError("Unknown API operation: " + apiCall, context); } break; default: throw CommandException.unknownCommand(context); } } @Override public Collection<CommandDescription> getCommandDescriptions() { // TODO possible expansion: add "local" query command, too? final Collection<CommandDescription> contributions = new ArrayList<CommandDescription>(); contributions .add(new CommandDescription(ROOT_COMMAND + SPACE + SUBCOMMAND_FETCH_LOCAL + "/" + SUBCOMMAND_FETCH_LOCAL_SHORT, "", false, "prints system monitoring data for the local instance")); contributions .add(new CommandDescription(ROOT_COMMAND + SPACE + SUBCOMMAND_FETCH_REMOTE + "/" + SUBCOMMAND_FETCH_REMOTE_SHORT, "", false, "fetches system monitoring data from all reachable nodes in the network, and prints it in a human-readable format")); contributions.add(new CommandDescription(ROOT_COMMAND + SPACE + SUBCOMMAND_API, "<operation>", false, "fetches system monitoring data from all reachable nodes in the network, and prints it in a parser-friendly format.", "Available operations:", " avgcpu+ram <time span> <time limit> - fetches the average CPU load over the given time span and the current free RAM", "Operation parameters:", " time span - the maximum time span (in seconds) to aggregate load data over", " time limit - the maximum time (in milliseconds) to wait for each node's load data response")); return contributions; } /** * 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 bindCommunicationService(CommunicationService newInstance) { this.communicationService = newInstance; } /** * OSGi-DS bind method. * * @param newInstance the new service instance */ public void bindPlatformService(PlatformService newInstance) { // only needed for fetching the local node id, so the service instance itself is not stored this.localInstanceNodeSessionId = newInstance.getLocalInstanceNodeSessionId(); } private void performPrintLocalSysMonData(final CommandContext context, int timeSpanMsec, int timeLimitMsec, boolean humanReadable) throws CommandException { // construct a set with only the local node id to only have a single code path for local and remote use cases final Set<InstanceNodeSessionId> singleNodeIdSet = new HashSet<>(); singleNodeIdSet.add(localInstanceNodeSessionId); // delegate performCollectAndPrintSysMonData(context, singleNodeIdSet, timeSpanMsec, timeLimitMsec, humanReadable); } private void performCollectAndPrintSysMonData(final CommandContext context, int timeSpanMsec, int timeLimitMsec, boolean humanReadable) throws CommandException { // TODO possible expansion: allow nodes to set a node property indicating whether they publish their load data or not? final Set<InstanceNodeSessionId> reachableInstanceNodes = communicationService.getReachableInstanceNodes(); // delegate performCollectAndPrintSysMonData(context, reachableInstanceNodes, timeSpanMsec, timeLimitMsec, humanReadable); } private void performCollectAndPrintSysMonData(final CommandContext context, Set<InstanceNodeSessionId> nodeIds, int timeSpanMsec, int timeLimitMsec, boolean humanReadable) throws CommandException { // fetch data final Map<InstanceNodeSessionId, SystemLoadInformation> resultMap; try { resultMap = localSystemMonitoringAggregationService .collectSystemMonitoringDataWithTimeLimit(nodeIds, timeSpanMsec, timeLimitMsec); } catch (InterruptedException | ExecutionException | TimeoutException e) { throw CommandException.executionError(e.toString(), context); } // select output format string final String formatString; if (humanReadable) { formatString = "%s - Average CPU load:%6.2f (%2d samples over %5d msec), Available RAM:%6d kiB"; } else { formatString = "id=%s CpuAvg=%.2f n=%d t=%d FreeRam=%d"; } // generate output final StringBuilder outputBuffer = new StringBuilder(); for (Entry<InstanceNodeSessionId, SystemLoadInformation> e : resultMap.entrySet()) { final SystemLoadInformation data = e.getValue(); final AverageOfDoubles cpuLoadAvg = data.getCpuLoadAvg(); // conversions (extracted for legibility) final String nodeIdString = e.getKey().getInstanceNodeSessionIdString(); final int cpuLoadAvgTimeSpan = cpuLoadAvg.getNumSamples() * LocalSystemMonitoringAggregationService.SYSTEM_LOAD_INFORMATION_COLLECTION_INTERVAL_MSEC; final double cpuAvgAsPercentage = cpuLoadAvg.getAverage() * 100; // append line to buffer, with no newline after the last one if (outputBuffer.length() != 0) { outputBuffer.append("\n"); } outputBuffer .append(StringUtils.format(formatString, nodeIdString, cpuAvgAsPercentage, cpuLoadAvg.getNumSamples(), cpuLoadAvgTimeSpan, data.getAvailableRam())); if (humanReadable) { outputBuffer.append(" ("); outputBuffer.append(e.getKey().getAssociatedDisplayName()); outputBuffer.append(")"); } } // print context.println(outputBuffer.toString()); } // TODO (p2) refactor into common utility method; duplicated in Remote Access plugin private int parseRequiredPositiveIntParameter(final CommandContext context, String name) throws CommandException { final String parameter = context.consumeNextToken(); if (parameter == null) { throw CommandException.wrongNumberOfParameters(context); } final int timespan; try { timespan = Integer.parseInt(parameter); if (timespan <= 0) { throw CommandException.syntaxError("The " + name + " parameter must be positive: " + parameter, context); } } catch (NumberFormatException e) { throw CommandException.syntaxError("The " + name + " parameter must be an integer number: " + parameter, context); } return timespan; } }