preparedstatement recording

This commit is contained in:
Sander Hautvast 2018-05-17 15:45:31 +02:00
parent 6459f35306
commit 7875c68ec3
9 changed files with 419 additions and 169 deletions

View file

@ -1,15 +1,32 @@
# perfix
Pretty basic profiling tool for JVM's
* agent that instruments loaded classes: -javaagent:perfix.jar
* include classes for instrumentation with -Dperfix.includes=com.project. ...etc (includes subpackages)
* ssh interface to report executed methods:
# Highlights:
* Meant for development time (after process stops, data is gone).
* Minimal memory footprint (agent is 2.5 mb).
* Minimalistic commandline interface.
* Execution time is measured in nanoseconds, reported in milliseconds (this way the totals and averages are most precise, but also human readable).
* No manual instrumentation necessary using loadtime bytecode manipulation (javassist).
* No special jdbc configuration necessary (ie no wrapped jdbc driver).
* The agent is also the server (unlike commercial tooling). This way there is no overhead in interprocess communication.
# Usage
* Agent that instruments loaded classes: -javaagent:<path>/perfix.jar
* Include classes for instrumentation with -Dperfix.includes=com.project. ...etc (includes subpackages)
* Ssh interface to report executed methods and sql query excutions:
<br/> * #invocations
<br/> * total execution time for the method in nanoseconds (this is also the sorting order)
<br/> * average time in nanoseconds per method (= total/#invocations)
* The server starts on port 2048 by default. Use -Dperfix.port=... to adjust.
* The (ssh) server starts on port 2048 by default. Use -Dperfix.port=... to adjust.
# roadmap
* finish jdbc query logging
* make output format configurable
* implement password login (now any)
* Overhead (in method execution time) not clear yet. I wouldn't use it in production.
* Finish jdbc query logging
* Make output format configurable
* Implement password login (now any)
* Add web interface (maybe)
* Implement an actual call stack the way commercial tools work
# DISCLAIMER:
This has only been tested on oracle java8 in spring-boot using tomcat web-container

24
agent.iml Normal file
View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8">
<output url="file://$MODULE_DIR$/target/classes" />
<output-test url="file://$MODULE_DIR$/target/test-classes" />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/deploy/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Maven: org.ow2.asm:asm:4.1" level="project" />
<orderEntry type="library" name="Maven: org.javassist:javassist:3.21.0-GA" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: junit:junit:4.12" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:1.3" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: com.h2database:h2:1.4.195" level="project" />
<orderEntry type="library" name="Maven: org.apache.sshd:sshd-core:1.7.0" level="project" />
<orderEntry type="library" name="Maven: org.slf4j:slf4j-api:1.7.25" level="project" />
<orderEntry type="library" name="Maven: org.slf4j:slf4j-nop:1.7.25" level="project" />
</component>
</module>

10
pom.xml
View file

@ -29,12 +29,18 @@
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.195</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.sshd</groupId>
<artifactId>sshd-core</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>1.7.25</version>
</dependency>
</dependencies>
<properties>
@ -43,9 +49,12 @@
</properties>
<build>
<testSourceDirectory>src/deploy/java</testSourceDirectory>
<plugins>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<manifestEntries>
@ -58,6 +67,7 @@
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<phase>package</phase>

View file

@ -5,31 +5,49 @@ import java.util.concurrent.TimeUnit;
public class Main {
public static void main(String[] args) {
System.out.println("Perfix Test Application is running. Make sure the agent is active.");
String includesProperty = System.getProperty("perfix.includes");
if (includesProperty == null || !includesProperty.equals("testperfix")) {
System.out.println("Start me with -javaagent:target/agent-0.1-SNAPSHOT.jar -Dperfix.includes=testperfix");
System.out.println("Then start putty (or other telnet client) and telnet to localhost:2048");
System.out.println("Exiting now");
System.exit(0);
}
System.out.println("Now start ssh session on localhost on port 2048 (without closing the Test Application)");
run();
}
public static void run() {
someJdbcMethod();
someOtherMethod();
private static void run() {
try {
Class.forName("org.h2.Driver");
someJdbcStatentMethod();
someJdbcPreparedStatementMethod();
someOtherMethod();
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
} catch (Exception e) {
e.printStackTrace();
}
}
private static void someJdbcMethod() {
private static void someJdbcStatentMethod() {
try {
Class.forName("org.h2.Driver");
Connection connection = DriverManager.getConnection("jdbc:h2:mem:default", "sa", "");
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("select CURRENT_DATE()");
while (resultSet.next()){
System.out.println("today is "+resultSet.getObject(1));
statement.executeQuery("select CURRENT_DATE() -- simple statement");
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
} catch (ClassNotFoundException | SQLException e) {
}
private static void someJdbcPreparedStatementMethod() {
try {
Connection connection = DriverManager.getConnection("jdbc:h2:mem:default", "sa", "");
PreparedStatement preparedStatement = connection.prepareStatement("select CURRENT_DATE() -- prepared statement");
preparedStatement.executeQuery();
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}

View file

@ -6,6 +6,7 @@ import perfix.server.SSHServer;
import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static java.util.Arrays.asList;
@ -17,149 +18,26 @@ public class Agent {
private static final String INCLUDES_PROPERTY = "perfix.includes";
private static final String DEFAULT_PORT = "2048";
private static final String MESSAGE = "Perfix agent active";
private static final String MESSAGE = " --- Perfix agent active --- ";
private static final String PERFIX_METHODINVOCATION_CLASS = "perfix.MethodInvocation";
private static ClassPool classpool;
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println(MESSAGE);
int port = Integer.parseInt(System.getProperty(PORT_PROPERTY, DEFAULT_PORT));
classpool = ClassPool.getDefault();
instrumentCode(inst);
new ClassInstrumentor(determineIncludes()).instrumentCode(inst);
new SSHServer().startListeningOnSocket(port);
}
private static void instrumentCode(Instrumentation inst) {
List<String> includes = determineIncludes();
inst.addTransformer((classLoader, resource, aClass, protectionDomain, uninstrumentedByteCode)
-> createByteCode(includes, resource, uninstrumentedByteCode));
}
private static byte[] createByteCode(List<String> includes, String resource, byte[] uninstrumentedByteCode) {
if (!isInnerClass(resource)) {
try {
CtClass ctClass = getCtClassForResource(resource);
if (isJdbcStatement(resource, ctClass)) {
return instrumentJdbcCalls(ctClass);
}
if (shouldInclude(resource, includes)) {
byte[] instrumentedBytecode = instrumentMethod(ctClass);
if (instrumentedBytecode != null) {
return instrumentedBytecode;
}
}
} catch (Exception ex) {
//suppress
}
}
return uninstrumentedByteCode;
}
private static byte[] instrumentJdbcCalls(CtClass classToInstrument) throws IOException, CannotCompileException {
try {
stream(classToInstrument.getDeclaredMethods("executeQuery")).forEach(m -> {
try {
m.insertBefore("System.out.println($1);");
} catch (CannotCompileException e) {
e.printStackTrace();
}
});
} catch (Exception e) {
e.printStackTrace();
}
byte[] byteCode = classToInstrument.toBytecode();
classToInstrument.detach();
return byteCode;
}
private static boolean isJdbcStatement(String resource, CtClass ctClass) throws NotFoundException {
if (!resource.startsWith("java/sql")) {
return stream(ctClass.getInterfaces())
.anyMatch(i -> i.getName().equals("java.sql.Statement") && !i.getName().equals("java.sql.PreparedStatement"));
}
return false;
}
private static byte[] instrumentMethod(CtClass classToInstrument) throws
NotFoundException, IOException, CannotCompileException {
CtClass perfixMethodInvocationClass = getCtClass(PERFIX_METHODINVOCATION_CLASS);
if (!classToInstrument.isInterface()) {
stream(classToInstrument.getDeclaredMethods()).forEach(m -> {
instrumentMethod(perfixMethodInvocationClass, m);
});
byte[] byteCode = classToInstrument.toBytecode();
classToInstrument.detach();
return byteCode;
} else {
return null;
}
}
private static CtClass getCtClassForResource(String resource) throws NotFoundException {
return getCtClass(resource.replaceAll("/", "."));
}
private static CtClass getCtClass(String classname) throws NotFoundException {
return classpool.get(classname);
}
private static void instrumentMethod(CtClass methodClass, CtMethod methodToinstrument) {
try {
methodToinstrument.addLocalVariable("perfixmethod", methodClass);
methodToinstrument.insertBefore("perfixmethod = perfix.MethodInvocation.start(\"" + methodToinstrument.getLongName() + "\");");
methodToinstrument.insertAfter("perfixmethod.stop();");
} catch (CannotCompileException e) {
throw new RuntimeException(e);
}
}
private static List<String> determineIncludes() {
return new ArrayList<>(asList(System.getProperty(INCLUDES_PROPERTY).split(",")));
}
private static boolean shouldInclude(String resource, List<String> excludes) {
BooleanWrapper included = new BooleanWrapper(false);
excludes.forEach(include -> {
if (resource.startsWith(include)) {
included.set(true);
}
});
return included.get();
}
private static boolean isInnerClass(String resource) {
return resource.contains("$");
}
static class BooleanWrapper {
boolean value;
BooleanWrapper(boolean value) {
this.value = value;
}
void set(boolean value) {
this.value = value;
}
boolean get() {
return value;
}
@Override
public String toString() {
return Boolean.toString(value);
String includesPropertyValue = System.getProperty(INCLUDES_PROPERTY);
if (includesPropertyValue==null){
System.out.println("WARNING: perfix.includes not set ");
return Collections.emptyList();
}
return new ArrayList<>(asList(includesPropertyValue.split(",")));
}
}

View file

@ -0,0 +1,284 @@
package perfix;
import javassist.*;
import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.util.List;
import static java.util.Arrays.stream;
class ClassInstrumentor {
private static final String JAVA_INNERCLASS_SEPARATOR = "$";
private static final String JAVA_STRING = "java.lang.String";
private static final String JAVASQL_PACKAGE = "java/sql";
private static final String JAVASQL_STATEMENT_INTERFACE = "java.sql.Statement";
private static final String JAVASQL_EXECUTE_METHOD = "execute";
private static final String JAVASQL_EXECUTEQUERY_METHOD = "executeQuery";
private static final String JAVASQL_EXECUTEUPDATE_METHOD = "executeUpdate";
private static final String JAVASQL_PREPAREDSTATEMENT_INTERFACE = "java.sql.PreparedStatement";
private static final String JAVASQL_CONNECTION_INTERFACE = "java.sql.Connection";
private static final String JAVASQL_PREPARED_STATEMENT_CLASSNAME = "java.sql.PreparedStatement";
private static final String JAVASQL_PREPAREDSTATEMENT_RESOURCENAME = "java/sql/PreparedStatement";
private static final String JAVASQL_PREPARESTATEMENT_METHODNAME = "prepareStatement";
private static final String JAVASSIST_FIRST_ARGUMENT_NAME = "$1";
private static final String JAVASSIST_RETURNVALUE = "$_";
private static final String PERFIX_METHODINVOCATION_CLASS = "perfix.MethodInvocation";
private static final String PERFIX_SQLSTATEMENT_FIELD = "_perfixSqlStatement";
private static final String PERFIX_SETSQL_METHOD = "setSqlForPerfix";
private ClassPool classpool;
private List<String> includes;
private CtClass perfixMethodInvocationClass;
private CtClass stringClass;
ClassInstrumentor(List<String> includes) {
this.includes = includes;
this.classpool = ClassPool.getDefault();
try {
perfixMethodInvocationClass = getCtClass(PERFIX_METHODINVOCATION_CLASS);
stringClass = classpool.get(JAVA_STRING);
} catch (NotFoundException e) {
//suppress TODO implement trace
}
}
void instrumentCode(Instrumentation inst) {
inst.addTransformer((classLoader, resource, aClass, protectionDomain, uninstrumentedByteCode)
-> instrumentCodeForClass(includes, resource, uninstrumentedByteCode));
}
private byte[] instrumentCodeForClass(List<String> includes, String resource, byte[] uninstrumentedByteCode) {
if (!isInnerClass(resource)) {
try {
CtClass ctClass = getCtClassForResource(resource);
if (isJdbcStatementImpl(resource, ctClass)) {
return instrumentJdbcStatement(ctClass, uninstrumentedByteCode);
}
if (isJdbcConnectionImpl(resource, ctClass)) {
return instrumentJdbcConnection(ctClass, uninstrumentedByteCode);
}
if (isJdbcPreparedStatement(resource)) {
return instrumentJdbcPreparedStatement(ctClass, uninstrumentedByteCode);
}
if (isJdbcPreparedStatementImpl(resource, ctClass)) {
return instrumentJdbcPreparedStatementImpl(ctClass, uninstrumentedByteCode);
}
if (shouldInclude(resource, includes)) {
return instrumentMethods(ctClass, uninstrumentedByteCode);
}
} catch (Exception ex) {
//suppress
}
}
return uninstrumentedByteCode;
}
/* Need to enhance interface to be able to set a statement (string) for perfix. */
private byte[] instrumentJdbcPreparedStatement(CtClass preparedStatementInterface, byte[] uninstrumentedByteCode) {
try {
preparedStatementInterface.getDeclaredMethod(PERFIX_SETSQL_METHOD);
} catch (NotFoundException e1) {
try {
CtMethod setSqlForPerfix = new CtMethod(CtClass.voidType, PERFIX_SETSQL_METHOD, new CtClass[]{stringClass}, preparedStatementInterface);
preparedStatementInterface.addMethod(setSqlForPerfix);
} catch (CannotCompileException e2) {
return uninstrumentedByteCode;
}
}
try {
return bytecode(preparedStatementInterface);
} catch (CannotCompileException | IOException e) {
return uninstrumentedByteCode;
}
}
/* Prepared statement methods that actually execute sql don't have the statement in their parameters (unlike java.sql.Statement)
* instead every jdbc vendor has a specific String field in their PreparedStatement impl that contains the statement.
*
* To circumvent vendor specifics in perfix, the first argument for prepareStatement (the sql String) is intercepted here
* and injected into the PreparedStatement instance under a fixed name, whatever the implementation type */
private byte[] instrumentJdbcConnection(CtClass classToInstrument, byte[] uninstrumentedByteCode) {
try {
stream(classToInstrument.getDeclaredMethods(JAVASQL_PREPARESTATEMENT_METHODNAME)).forEach(method -> {
try {
// The sql statement String that is the first argument for this method is injected into PreparedStatementImpl
// using a name known (only) to perfix, so that it can fetch it later in that class (instance)
// this way no JDBC implementor specific code is needed
CtClass preparedStatementInterface = classpool.get(JAVASQL_PREPARED_STATEMENT_CLASSNAME);
try {
preparedStatementInterface.getDeclaredMethod(PERFIX_SETSQL_METHOD);
} catch (NotFoundException e1) {
CtMethod setSqlForPerfix = new CtMethod(CtClass.voidType, PERFIX_SETSQL_METHOD, new CtClass[]{stringClass}, preparedStatementInterface);
preparedStatementInterface.addMethod(setSqlForPerfix);
}
method.insertAfter(JAVASSIST_RETURNVALUE + "." + PERFIX_SETSQL_METHOD + "(" + JAVASSIST_FIRST_ARGUMENT_NAME + ");"); //$_ is result instance, $1 is first argument
} catch (CannotCompileException | NotFoundException e) {
// suppress
e.printStackTrace();
}
}
);
return bytecode(classToInstrument);
} catch (NotFoundException | CannotCompileException | IOException e) {
// suppress
}
return uninstrumentedByteCode;
}
/* */
private byte[] instrumentJdbcPreparedStatementImpl(CtClass preparedStatementClass, byte[] uninstrumentedByteCode) {
try {
addPerfixStatementField(preparedStatementClass);
addPerfixStatementSetter(preparedStatementClass);
// instrument execute to record query duration
stream(preparedStatementClass.getDeclaredMethods(JAVASQL_EXECUTE_METHOD)).forEach(method ->
instrumentMethod(method, PERFIX_SQLSTATEMENT_FIELD)
);
// instrument executeQuery to record query duration
stream(preparedStatementClass.getDeclaredMethods(JAVASQL_EXECUTEQUERY_METHOD)).forEach(method ->
instrumentMethod(method, PERFIX_SQLSTATEMENT_FIELD)
);
// instrument executeUpdate to record query duration
stream(preparedStatementClass.getDeclaredMethods(JAVASQL_EXECUTEUPDATE_METHOD)).forEach(method ->
instrumentMethod(method, PERFIX_SQLSTATEMENT_FIELD)
);
return bytecode(preparedStatementClass);
} catch (NotFoundException | CannotCompileException | IOException e) {
e.printStackTrace();
return uninstrumentedByteCode;
}
}
private byte[] instrumentJdbcStatement(CtClass classToInstrument, byte[] uninstrumentedByteCode) {
try {
//instrument executeQuery to record query duration
stream(classToInstrument.getDeclaredMethods(JAVASQL_EXECUTEQUERY_METHOD)).forEach(method ->
instrumentMethod(method, JAVASSIST_FIRST_ARGUMENT_NAME)
);
//instrument executeUpdate to record query duration
stream(classToInstrument.getDeclaredMethods(JAVASQL_EXECUTEUPDATE_METHOD)).forEach(method ->
instrumentMethod(method, JAVASSIST_FIRST_ARGUMENT_NAME)
);
return bytecode(classToInstrument);
} catch (Exception e) {
return uninstrumentedByteCode;
}
}
/* for regular classes that require instrumentation instrument all methods to record duration*/
private byte[] instrumentMethods(CtClass classToInstrument, byte[] uninstrumentedByteCode) {
if (!classToInstrument.isInterface()) {
stream(classToInstrument.getDeclaredMethods()).forEach(this::instrumentMethod);
try {
return bytecode(classToInstrument);
} catch (IOException | CannotCompileException e) {
//suppress
}
}
return uninstrumentedByteCode;
}
private CtClass getCtClassForResource(String resource) throws NotFoundException {
return getCtClass(resource.replaceAll("/", "."));
}
private CtClass getCtClass(String classname) throws NotFoundException {
return classpool.get(classname);
}
private void instrumentMethod(CtMethod methodToinstrument) {
instrumentMethod(methodToinstrument, "\"" + methodToinstrument.getLongName() + "\"");
}
/* record times at beginning and end of method body*/
private void instrumentMethod(CtMethod methodToinstrument, String metricName) {
try {
methodToinstrument.addLocalVariable("_perfixmethod", perfixMethodInvocationClass);
methodToinstrument.insertBefore("_perfixmethod = perfix.MethodInvocation.start(" + metricName + ");");
methodToinstrument.insertAfter("perfix.MethodInvocation.stop(_perfixmethod);");
} catch (CannotCompileException e) {
throw new RuntimeException(e);
}
}
private boolean shouldInclude(String resource, List<String> excludes) {
MutableBoolean included = new MutableBoolean(false);
excludes.forEach(include -> {
if (resource.startsWith(include)) {
included.set(true);
}
});
return included.get();
}
private boolean isInnerClass(String resource) {
return resource.contains(JAVA_INNERCLASS_SEPARATOR);
}
private boolean isJdbcStatementImpl(String resource, CtClass ctClass) throws NotFoundException {
if (!resource.startsWith(JAVASQL_PACKAGE)) {
return stream(ctClass.getInterfaces())
.anyMatch(i -> i.getName().equals(JAVASQL_STATEMENT_INTERFACE) && !i.getName().equals(JAVASQL_PREPARED_STATEMENT_CLASSNAME));
}
return false;
}
private boolean isJdbcPreparedStatement(String resource) {
return resource.equals(JAVASQL_PREPAREDSTATEMENT_RESOURCENAME);
}
private boolean isJdbcPreparedStatementImpl(String resource, CtClass ctClass) throws NotFoundException {
if (!resource.startsWith(JAVASQL_PACKAGE)) {
return stream(ctClass.getInterfaces())
.anyMatch(i -> i.getName().equals(JAVASQL_PREPAREDSTATEMENT_INTERFACE));
}
return false;
}
private boolean isJdbcConnectionImpl(String resource, CtClass ctClass) throws NotFoundException {
if (!resource.startsWith(JAVASQL_PACKAGE)) {
return stream(ctClass.getInterfaces())
.anyMatch(i -> i.getName().equals(JAVASQL_CONNECTION_INTERFACE));
}
return false;
}
private byte[] bytecode(CtClass classToInstrument) throws IOException, CannotCompileException {
classToInstrument.detach();
return classToInstrument.toBytecode();
}
private void addPerfixStatementField(CtClass preparedStatementClass) throws CannotCompileException {
// add a String field that will contain the statement
CtField perfixSqlField = new CtField(stringClass, PERFIX_SQLSTATEMENT_FIELD, preparedStatementClass);
perfixSqlField.setModifiers(Modifier.PRIVATE);
preparedStatementClass.addField(perfixSqlField);
}
private void addPerfixStatementSetter(CtClass preparedStatementImplClass) throws CannotCompileException {
// add setter for the new field
CtMethod setSqlForPerfix = new CtMethod(CtClass.voidType, PERFIX_SETSQL_METHOD, new CtClass[]{stringClass}, preparedStatementImplClass);
setSqlForPerfix.setModifiers(Modifier.PUBLIC);
setSqlForPerfix.setBody(PERFIX_SQLSTATEMENT_FIELD + "=" + JAVASSIST_FIRST_ARGUMENT_NAME + ";");
preparedStatementImplClass.addMethod(setSqlForPerfix);
}
}

View file

@ -13,13 +13,16 @@ public class MethodInvocation {
public static MethodInvocation start(String name) {
return new MethodInvocation(name);
}
public static void stop(MethodInvocation methodInvocation) {
methodInvocation.stop();
}
public void stop() {
t1 = System.nanoTime();
Registry.add(this);
}
public String getName() {
String getName() {
return name;
}

View file

@ -0,0 +1,22 @@
package perfix;
public class MutableBoolean {
private boolean value;
MutableBoolean(boolean value) {
this.value = value;
}
void set(boolean value) {
this.value = value;
}
boolean get() {
return value;
}
@Override
public String toString() {
return Boolean.toString(value);
}
}

View file

@ -11,35 +11,31 @@ import java.io.PrintStream;
public class SshSessionInstance implements Command, Runnable {
private static final String ANSI_LOCAL_ECHO = "\u001B[12l";
private static final String ANSI_NEWLINE_CRLF = "\u001B[20h";
private InputStream is;
private OutputStream os;
private InputStream input;
private OutputStream output;
private ExitCallback callback;
private Thread sshThread;
@Override
public void start(Environment env) {
sshThread = new Thread(this, "EchoShell");
sshThread.start();
new Thread(this, "PerfixShell").start();
}
@Override
public void run() {
try {
os.write("press [enter] for report or [q] to quit\n".getBytes());
os.write((ANSI_LOCAL_ECHO + ANSI_NEWLINE_CRLF).getBytes());
os.flush();
output.write("press [enter] for report or [q] to quit\n".getBytes());
output.write((ANSI_NEWLINE_CRLF).getBytes());
output.flush();
boolean exit = false;
while (!exit) {
char c = (char) is.read();
char c = (char) input.read();
if (c == 'q') {
exit = true;
} else if (c == '\n') {
Registry.report(new PrintStream(os));
Registry.report(new PrintStream(output));
}
}
} catch (Exception e) {
@ -50,9 +46,7 @@ public class SshSessionInstance implements Command, Runnable {
}
@Override
public void destroy() {
sshThread.interrupt();
}
public void destroy() { }
@Override
public void setErrorStream(OutputStream errOS) {
@ -65,12 +59,12 @@ public class SshSessionInstance implements Command, Runnable {
@Override
public void setInputStream(InputStream is) {
this.is = is;
this.input = is;
}
@Override
public void setOutputStream(OutputStream os) {
this.os = os;
this.output = os;
}
}