/*
* Copyright 2014-2016 CyberVision, 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 org.kaaproject.kaa.server.common.thrift.cli.server;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.PosixParser;
import org.apache.thrift.TException;
import org.kaaproject.kaa.server.common.thrift.gen.cli.CliThriftService;
import org.kaaproject.kaa.server.common.thrift.gen.cli.CommandResult;
import org.kaaproject.kaa.server.common.thrift.gen.cli.CommandStatus;
import org.kaaproject.kaa.server.common.thrift.gen.cli.MemoryUsage;
import org.kaaproject.kaa.server.common.thrift.util.ThriftExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.PrintWriter;
import java.lang.Thread.State;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* The Class BaseCliThriftService.<br>
* Basic abstract class implementing default thrift CLI commands
*/
public abstract class BaseCliThriftService implements CliThriftService.Iface {
/**
* The Constant LOG.
*/
private static final Logger LOG = LoggerFactory
.getLogger(BaseCliThriftService.class);
/**
* The Constant MBYTE.
*/
private static final long MBYTE = 1024 * 1024;
/**
* The Thrift CLI commands map.
*/
private Map<String, Command> commandsMap = new LinkedHashMap<>();
/**
* Instantiates a new base cli thrift service.
*/
public BaseCliThriftService() {
initDefaultCommands();
initServiceCommands();
}
/**
* Inits the default thrift CLI commands.
*/
private void initDefaultCommands() {
Command helpCommand = new Command("help", "display available commands") {
@Override
public void runCommand(CommandLine line, PrintWriter writer) {
listCommands(writer);
}
};
addCommand(helpCommand);
Command memoryCommand = new Command("memory",
"display server memory info") {
@Override
public void runCommand(CommandLine line, PrintWriter writer) {
printMemory(writer, line.hasOption('g'));
}
};
addCommand(memoryCommand);
memoryCommand.addOption(new Option("g", "gc", false,
"Force Garbage Collector before memory status"));
Command threadsCommand = new Command("threads", "dump JVM threads") {
@Override
public void runCommand(CommandLine line, PrintWriter writer) {
dumpThreads(writer);
}
};
addCommand(threadsCommand);
Command shutdownCommand = new Command("shutdown", "shutdown server") {
@Override
public void runCommand(CommandLine line, PrintWriter writer) {
shutdown(writer);
}
};
addCommand(shutdownCommand);
}
/**
* Gets the thrift server short name used to display in thrift cli console.
*
* @return the server short name
*/
protected abstract String getServerShortName();
/**
* Inits the service specific CLI commands.
*/
protected abstract void initServiceCommands();
/**
* Adds the CLI command and inits default command options.
*
* @param command the command
*/
protected void addCommand(Command command) {
command.addOption(new Option("h", "help", false,
"Print command help information"));
commandsMap.put(command.getCommand(), command);
}
/*
* (non-Javadoc)
*
* @see
* org.kaaproject.kaa.server.common.thrift.gen.cli.CliThriftService
* .Iface#serverName()
*/
@Override
public String serverName() throws TException {
return getServerShortName();
}
/*
* (non-Javadoc)
*
* @see
* org.kaaproject.kaa.server.common.thrift.gen.cli.CliThriftService
* .Iface#shutdown()
*/
@Override
public void shutdown() throws TException {
LOG.info("Received shutdown command.");
Runnable shutdownCommand = new Runnable() {
@Override
public void run() {
try {
ThriftExecutor.shutdown();
System.exit(0); //NOSONAR
} catch (Exception ex) {
LOG.error("Catch exception when execute shutdown command", ex);
}
}
};
new Thread(shutdownCommand).start();
}
/**
* Shutdown server.
*
* @param writer the writer to output shutdown information.
*/
private void shutdown(PrintWriter writer) {
writer.println("Server shutdown initiated.");
try {
shutdown();
} catch (TException ex) {
LOG.error("Catch exception when execute shutdown command", ex);
}
}
/*
* (non-Javadoc)
*
* @see
* org.kaaproject.kaa.server.common.thrift.gen.cli.CliThriftService
* .Iface#getMemoryUsage(boolean)
*/
@Override
public MemoryUsage getMemoryUsage(boolean forceGc)
throws TException {
if (forceGc) {
System.gc(); //NOSONAR
}
MemoryUsage memUsage = new MemoryUsage();
memUsage.setMax(Runtime.getRuntime().maxMemory());
memUsage.setFree(Runtime.getRuntime().freeMemory());
memUsage.setTotal(Runtime.getRuntime().totalMemory());
return memUsage;
}
/*
* (non-Javadoc)
*
* @see
* org.kaaproject.kaa.server.common.thrift.gen.cli.CliThriftService
* .Iface#executeCommand(java.lang.String)
*/
@Override
public CommandResult executeCommand(String commandLineString)
throws TException {
CommandStatus status = CommandStatus.OK;
String[] args = commandLineString.split(" ");
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
PrintWriter writer = new PrintWriter(outStream);
String commandString = args[0];
Command command = commandsMap.get(commandString);
if (command != null) {
CommandLineParser cmdLinePosixParser = new PosixParser();
String[] commandArgs = new String[args.length - 1];
System.arraycopy(args, 1, commandArgs, 0, args.length - 1);
try {
CommandLine commandLine = cmdLinePosixParser.parse(
command.getOptions(), commandArgs);
if (commandLine.hasOption('h')) {
printHelp(command, writer);
} else {
command.runCommand(commandLine, writer);
}
} catch (ParseException ex) {
writer.println("Unable to parse command arguments.");
writer.println();
printHelp(command, writer);
status = CommandStatus.ERROR;
}
} else {
writer.println("Error: unknown command '" + commandString + "'");
listCommands(writer);
}
CommandResult result = new CommandResult();
writer.println();
writer.flush();
result.message = outStream.toString();
result.status = status;
return result;
}
/**
* thrift cli console help command to display available commands.
*
* @param writer the writer to write help output
*/
private void listCommands(PrintWriter writer) {
writer.println("Available commands:");
writer.println();
int max = 0;
for (String command : commandsMap.keySet()) {
max = Math.max(max, command.length());
}
for (String command : commandsMap.keySet()) {
StringBuffer commandBuf = new StringBuffer();
commandBuf.append(command);
if (commandBuf.length() < max) {
commandBuf.append(createPadding(max - commandBuf.length()));
}
String desc = commandsMap.get(command).getDesc();
commandBuf.append(" ");
commandBuf.append(desc);
writer.println(commandBuf.toString());
}
writer.println();
writer.println("To see command usage execute: <command> -h");
}
/**
* Prints service memory usage.
*
* @param writer the writer to output memory usage info
* @param forceGc force Garbage Collection before obtaining memory information
*/
private void printMemory(PrintWriter writer, boolean forceGc) {
writer.println("Memory Usage:");
writer.println();
try {
MemoryUsage memUsage = getMemoryUsage(forceGc);
NumberFormat format = new DecimalFormat("0.#");
writer.println("Max available : "
+ format.format((float) memUsage.max / (float) MBYTE)
+ " MBytes");
writer.println("Current heap : "
+ format.format((float) memUsage.total / (float) MBYTE)
+ " MBytes");
writer.println("Used : "
+ format.format((float) (memUsage.total - memUsage.free)
/ (float) MBYTE) + " MBytes");
} catch (TException ex) {
LOG.error("Catch exception when execute print memory command", ex);
}
}
/**
* Dump service threads information.
*
* @param writer the writer to output threads information
*/
private void dumpThreads(PrintWriter writer) {
writer.println("THREADS DUMP:");
writer.println();
ThreadMXBean threadMxBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threads = threadMxBean.dumpAllThreads(false, false);
Map<Long, ThreadStruct> threadsMap = new HashMap<>();
for (ThreadInfo ti : threads) {
ThreadStruct ts = new ThreadStruct();
ts.ti = ti;
threadsMap.put(ti.getThreadId(), ts);
}
ThreadGroup root = Thread.currentThread().getThreadGroup();
ThreadGroup parent;
parent = root.getParent();
while (parent != null) {
root = parent;
parent = parent.getParent();
}
allThreadsFromGroup(root, threadsMap);
Collection<ThreadStruct> threadValues = threadsMap.values();
List<ThreadStruct> threadList = new ArrayList<>(
threadValues);
Collections.sort(threadList);
Map<State, Integer> threadStatistics = new LinkedHashMap<>();
threadStatistics.put(State.NEW, 0);
threadStatistics.put(State.RUNNABLE, 0);
threadStatistics.put(State.BLOCKED, 0);
threadStatistics.put(State.WAITING, 0);
threadStatistics.put(State.TIMED_WAITING, 0);
threadStatistics.put(State.TERMINATED, 0);
int maxGroup = 0;
int maxName = 0;
for (ThreadStruct thread : threadList) {
maxName = Math.max(thread.ti.getThreadName().length(), maxName);
maxGroup = Math.max(thread.getGroupName().length(), maxGroup);
int count = threadStatistics.get(thread.ti.getThreadState());
count++;
threadStatistics.put(thread.ti.getThreadState(), count);
}
StringBuffer header = new StringBuffer();
header.append("ID");
int idColumnLength = 4;
int length = idColumnLength;
header.append(createPadding(length - header.length()));
header.append("GROUP");
int groupColumnLength = maxGroup + 1;
length += groupColumnLength;
header.append(createPadding(length - header.length()));
header.append("NAME");
int nameColumnLength = maxName + 1;
length += nameColumnLength;
header.append(createPadding(length - header.length()));
header.append("PRIORITY");
int priorityColumnLength = 10;
length += priorityColumnLength;
header.append(createPadding(length - header.length()));
header.append("STATE");
int stateColumnLength = 14;
length += stateColumnLength;
header.append(createPadding(length - header.length()));
header.append("DAEMON");
int daemonColumnLengh = 7;
length += daemonColumnLengh;
header.append(createPadding(length - header.length()));
header.append("ALIVE");
int aliveColumnLengh = 6;
length += aliveColumnLengh;
header.append(createPadding(length - header.length()));
header.append("CPU TIME (SEC)");
int cpuTimeColumnLengh = 14;
length += cpuTimeColumnLengh;
header.append(createPadding(length - header.length()));
writer.println(header);
int maxRowLength = header.length();
writer.println(createPadding(maxRowLength, '-'));
NumberFormat format = new DecimalFormat("0.#");
for (ThreadStruct thread : threadList) {
StringBuffer row = new StringBuffer();
row.append(thread.ti.getThreadId());
int rowLength = idColumnLength;
row.append(createPadding(rowLength - row.length()));
row.append(thread.getGroupName());
rowLength += groupColumnLength;
row.append(createPadding(rowLength - row.length()));
row.append(thread.ti.getThreadName());
rowLength += nameColumnLength;
row.append(createPadding(rowLength - row.length()));
row.append(thread.getPriority());
rowLength += priorityColumnLength;
row.append(createPadding(rowLength - row.length()));
row.append(thread.ti.getThreadState());
rowLength += stateColumnLength;
row.append(createPadding(rowLength - row.length()));
row.append(thread.isDaemon());
rowLength += daemonColumnLengh;
row.append(createPadding(rowLength - row.length()));
row.append(thread.isAlive());
rowLength += aliveColumnLengh;
row.append(createPadding(rowLength - row.length()));
double cpuTimeSec = (double) threadMxBean
.getThreadCpuTime(thread.ti.getThreadId())
/ (double) (1000 * 1000 * 1000);
row.append(format.format(cpuTimeSec));
writer.println(row);
}
writer.println(createPadding(maxRowLength, '-'));
writer.println("SUMMARY:");
writer.println(createPadding(maxRowLength, '-'));
for (State state : threadStatistics.keySet()) {
int count = threadStatistics.get(state);
if (count > 0) {
StringBuffer row = new StringBuffer();
row.append(state.toString());
row.append(createPadding(stateColumnLength - row.length()));
row.append(count);
writer.println(row);
}
}
writer.println(createPadding(maxRowLength, '-'));
StringBuffer row = new StringBuffer();
row.append("TOTAL");
row.append(createPadding(stateColumnLength - row.length()));
row.append(threadList.size());
writer.println(row);
writer.println(createPadding(maxRowLength, '-'));
}
/**
* Retrieve all threads info from thread group.
*
* @param group the thread group
* @param threadsMap the threads map to store threads info
*/
private void allThreadsFromGroup(ThreadGroup group,
Map<Long, ThreadStruct> threadsMap) {
int threadCount = group.activeCount();
int groupCount = group.activeGroupCount();
Thread[] groupThreads = new Thread[threadCount];
ThreadGroup[] threadGroups = new ThreadGroup[groupCount];
group.enumerate(groupThreads, false);
group.enumerate(threadGroups, false);
for (Thread t : groupThreads) {
if (t != null) {
ThreadStruct ts = threadsMap.get(t.getId());
ts.thread = t;
}
}
for (ThreadGroup tg : threadGroups) {
allThreadsFromGroup(tg, threadsMap);
}
}
/**
* Creates the string padding.
*
* @param len length of the padding
* @return the resulting padding string
*/
protected String createPadding(int len) {
StringBuffer sb = new StringBuffer(len);
for (int i = 0; i < len; ++i) {
sb.append(' ');
}
return sb.toString();
}
/**
* Creates the padding using specified character.
*
* @param len length of the padding
* @param character the character to use for padding
* @return the resulting padding string
*/
protected String createPadding(int len, char character) {
StringBuffer sb = new StringBuffer(len);
for (int i = 0; i < len; ++i) {
sb.append(character);
}
return sb.toString();
}
/**
* Prints the CLI command usage.
*
* @param command the CLI command
* @param writer the writer to write usage output
*/
private void printHelp(Command command, PrintWriter writer) {
writer.println(command.getCommand() + " - " + command.getDesc());
writer.println();
HelpFormatter helpFormatter = new HelpFormatter();
helpFormatter.printHelp(writer, 80, command.getCommand(), "Options",
command.getOptions(), 3, 5, "", true);
}
/**
* The Class ThreadStruct. Used to store thread information. Implements
* comparison mechanism to sort threads information.
*/
class ThreadStruct implements Comparable<ThreadStruct> {
/**
* The thread.
*/
public Thread thread;
/**
* The thread info.
*/
public ThreadInfo ti;
/**
* Gets the thread group name.
*
* @return the group name
*/
public String getGroupName() {
return thread != null ? thread.getThreadGroup().getName() : "";
}
/**
* Gets the thread priority.
*
* @return the priority
*/
public String getPriority() {
return thread != null ? thread.getPriority() + "" : "";
}
/**
* Checks if thread is daemon.
*
* @return the "daemon" string if thread is daemon otherwise empty string
*/
public String isDaemon() {
return thread != null && thread.isDaemon() ? "daemon" : "";
}
/**
* Checks if thread is alive.
*
* @return the "alive" string if thread is alive otherwise empty string
*/
public String isAlive() {
return thread != null && thread.isAlive() ? "alive" : "";
}
/*
* (non-Javadoc)
*
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
@Override
public int compareTo(ThreadStruct obj) {
int result = getGroupName().compareTo(obj.getGroupName());
if (result == 0) {
result = (int) (ti.getThreadId() - obj.ti.getThreadId());
}
return result;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof ThreadStruct)) {
return false;
}
ThreadStruct that = (ThreadStruct) obj;
if (thread != null ? !thread.equals(that.thread) : that.thread != null) {
return false;
}
if (ti != null ? !ti.equals(that.ti) : that.ti != null) {
return false;
}
return true;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#hashCode(java.lang.Object)
*/
@Override
public int hashCode() {
int result = thread != null ? thread.hashCode() : 0;
result = 31 * result + (ti != null ? ti.hashCode() : 0);
return result;
}
}
}