package org.threadly.litesockets.networkutils;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.concurrent.ConcurrentHashMap;
import org.threadly.concurrent.SubmitterScheduler;
import org.threadly.litesockets.Client;
import org.threadly.litesockets.Client.CloseListener;
import org.threadly.litesockets.Client.Reader;
import org.threadly.litesockets.Server.ClientAcceptor;
import org.threadly.litesockets.SocketExecuter;
import org.threadly.litesockets.TCPServer;
import org.threadly.litesockets.buffers.ReuseableMergedByteBuffers;
import org.threadly.util.AbstractService;
import org.threadly.util.ExceptionUtils;
import org.threadly.util.debug.Profiler;
/**
* <p>The ProfileServer Uses Threadly's {@link Profiler} tying it to a listen socket to make it easy
* to profile running java processes as needed.</p>
*
* This will open a listen port on specified ip/port and allow connections to send basic commands to the
* process to control the profiler.
*
* Here are a list of commands:
*
* start - Starts the profiler
* stop - Stops the profiler (Profile is maintained)
* reset - Resets the profilers data
* dump - dumps the profiler current data
*
* Commands must end with a newline.
*
* NOTE: the profiler should only be used when needed and stopped and reset when not in use. The longer it
* runs the more CPU and Memory it will consume, to the point where it could eat up an entire CPU core and over fill
* memory.
*
*/
public class ProfileServer extends AbstractService implements ClientAcceptor, Reader, CloseListener{
public static final int DISCONNECT_DELAY = 500;
private static final Charset DEFAULT_CHARSET = Charset.forName("US-ASCII");
protected static final ByteBuffer DUMP_EXCEPTION = ByteBuffer.wrap("Got Exception doing Dump!\n\n".getBytes(DEFAULT_CHARSET)).asReadOnlyBuffer();
protected static final ByteBuffer BAD_DATA = ByteBuffer.wrap("Got Bad Data from you, closing!!\n\n".getBytes(DEFAULT_CHARSET)).asReadOnlyBuffer();
protected static final ByteBuffer STARTED_RESPONSE = ByteBuffer.wrap("Profiler Started\n\n".getBytes(DEFAULT_CHARSET)).asReadOnlyBuffer();
protected static final ByteBuffer ALREADY_STARTED_RESPONSE = ByteBuffer.wrap("Profiler Already Started\n\n".getBytes(DEFAULT_CHARSET)).asReadOnlyBuffer();
protected static final ByteBuffer STOPPED_RESPONSE = ByteBuffer.wrap("Profiler Stopped\n\n".getBytes(DEFAULT_CHARSET)).asReadOnlyBuffer();
protected static final ByteBuffer ALREADY_STOPPED_RESPONSE = ByteBuffer.wrap("Profiler is not Running\n\n".getBytes(DEFAULT_CHARSET)).asReadOnlyBuffer();
protected static final ByteBuffer RESET_RESPONSE = ByteBuffer.wrap("Profiler Reset\n\n".getBytes(DEFAULT_CHARSET)).asReadOnlyBuffer();
protected static final String START_DUMP = "---------------------START-DUMP---------------------------------\n";
protected static final String END_DUMP = "\n---------------------END-DUMP-----------------------------------\n\n";
protected static final String START_PROFILE = "start";
protected static final String STOP_PROFILE = "stop";
protected static final String RESET_PROFILE = "reset";
protected static final String DUMP_PROFILE = "dump";
protected static final ByteBuffer HELP;
static {
final StringBuilder sb = new StringBuilder(150);
sb.append("HELP MENU:\n");
sb.append(START_PROFILE).append(" - Starts the profiler\n");
sb.append(STOP_PROFILE).append(" - Stops the profiler (Profile is maintained)\n");
sb.append(RESET_PROFILE).append(" - Resets the profilers data\n");
sb.append(DUMP_PROFILE).append(" - dumps the profiler current data\n");
HELP = ByteBuffer.wrap(sb.toString().getBytes(DEFAULT_CHARSET)).asReadOnlyBuffer();
}
private final SubmitterScheduler scheduler;
private final SocketExecuter socketEx;
private final ConcurrentHashMap<Client, ReuseableMergedByteBuffers> clients = new ConcurrentHashMap<Client, ReuseableMergedByteBuffers>();
private final Profiler profiler;
private final String host;
private final int port;
private TCPServer server;
/**
* Constructs a ProileServer.
*
* @param socketEx The SocketExecuterInterface to use with this Server.
* @param host the host to create the servers listen port on.
* @param port the port to use.
* @param frequency the frequency of the profiler in ms.
*/
public ProfileServer(final SocketExecuter socketEx, final String host, final int port, final int frequency) {
socketEx.startIfNotStarted();
scheduler = socketEx.getThreadScheduler();
this.socketEx = socketEx;
profiler = new Profiler(frequency);
this.host = host;
this.port = port;
}
@Override
public void onClose(final Client client) {
clients.remove(client);
}
@Override
public void onRead(final Client client) {
final ReuseableMergedByteBuffers mbb = clients.get(client);
mbb.add(client.getRead());
int pos = -1;
while((pos = mbb.indexOf("\n")) > -1) {
final String cmd = mbb.getAsString(pos).trim().toLowerCase();
mbb.discard(1);
if(START_PROFILE.equals(cmd)) {
if(profiler.isRunning()) {
client.write(ALREADY_STARTED_RESPONSE.duplicate());
} else {
profiler.start();
client.write(STARTED_RESPONSE.duplicate());
}
} else if(STOP_PROFILE.equals(cmd)) {
if(profiler.isRunning()) {
profiler.stop();
client.write(STOPPED_RESPONSE.duplicate());
} else {
client.write(ALREADY_STOPPED_RESPONSE.duplicate());
}
} else if(RESET_PROFILE.equals(cmd)) {
profiler.reset();
client.write(RESET_RESPONSE.duplicate());
} else if(DUMP_PROFILE.equals(cmd)) {
dumpProfile(client);
} else {
sendHelp(client);
}
}
if(mbb.remaining() > 100) {
client.setReader(null);
client.write(BAD_DATA.duplicate()).addListener(new Runnable() {
@Override
public void run() {
client.close();
}});
}
}
@Override
public void accept(final Client client){
try {
clients.put(client, new ReuseableMergedByteBuffers());
client.setReader(this);
client.addCloseListener(this);
//socketEx.addClient(client);
} catch (Exception e) {
ExceptionUtils.handleException(e);
}
}
private void sendHelp(final Client client) {
scheduler.execute(new Runnable() {
@Override
public void run() {
client.write(HELP.duplicate());
}
});
}
private void dumpProfile(final Client client) {
scheduler.execute(new Runnable() {
@Override
public void run() {
final ByteArrayOutputStream baos = new ByteArrayOutputStream(0);
final OutputStream os = new BufferedOutputStream(baos);
try {
os.write(START_DUMP.getBytes(DEFAULT_CHARSET));
profiler.dump(os);
os.write(END_DUMP.getBytes(DEFAULT_CHARSET));
os.write("\n".getBytes(DEFAULT_CHARSET));
} catch(IOException e) {
client.write(DUMP_EXCEPTION.duplicate());
client.write(ByteBuffer.wrap(ExceptionUtils.stackToString(e).getBytes(DEFAULT_CHARSET)));
} finally {
try {
os.close();
} catch (IOException e) {
}
}
client.write(ByteBuffer.wrap(baos.toByteArray()));
}
});
}
@Override
protected void startupService() {
try {
server = socketEx.createTCPServer(host, port);
server.setClientAcceptor(this);
server.start();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
protected void shutdownService() {
profiler.stop();
profiler.reset();
server.close();
}
}