From 7875c68ec34857a2acb03bb589b640423c0d9e5b Mon Sep 17 00:00:00 2001 From: Sander Hautvast Date: Thu, 17 May 2018 15:45:31 +0200 Subject: [PATCH] preparedstatement recording --- README.md | 31 +- agent.iml | 24 ++ pom.xml | 10 + src/deploy/java/testperfix/Main.java | 44 ++- src/main/java/perfix/Agent.java | 140 +-------- src/main/java/perfix/ClassInstrumentor.java | 284 ++++++++++++++++++ src/main/java/perfix/MethodInvocation.java | 5 +- src/main/java/perfix/MutableBoolean.java | 22 ++ .../perfix/server/SshSessionInstance.java | 28 +- 9 files changed, 419 insertions(+), 169 deletions(-) create mode 100644 agent.iml create mode 100644 src/main/java/perfix/ClassInstrumentor.java create mode 100644 src/main/java/perfix/MutableBoolean.java diff --git a/README.md b/README.md index 1393bef..c38b6f8 100644 --- a/README.md +++ b/README.md @@ -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:/perfix.jar +* Include classes for instrumentation with -Dperfix.includes=com.project. ...etc (includes subpackages) +* Ssh interface to report executed methods and sql query excutions:
* #invocations
* total execution time for the method in nanoseconds (this is also the sorting order)
* 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 diff --git a/agent.iml b/agent.iml new file mode 100644 index 0000000..86526a8 --- /dev/null +++ b/agent.iml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index e14e232..0b60736 100644 --- a/pom.xml +++ b/pom.xml @@ -29,12 +29,18 @@ com.h2database h2 1.4.195 + test org.apache.sshd sshd-core 1.7.0 + + org.slf4j + slf4j-nop + 1.7.25 + @@ -43,9 +49,12 @@ + src/deploy/java + maven-jar-plugin + 3.1.0 @@ -58,6 +67,7 @@ maven-shade-plugin + 3.1.1 package diff --git a/src/deploy/java/testperfix/Main.java b/src/deploy/java/testperfix/Main.java index 816c023..14f51a6 100644 --- a/src/deploy/java/testperfix/Main.java +++ b/src/deploy/java/testperfix/Main.java @@ -5,31 +5,49 @@ import java.util.concurrent.TimeUnit; public class Main { public static void main(String[] args) { - 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("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("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)); - } - } catch (ClassNotFoundException | SQLException e) { + statement.executeQuery("select CURRENT_DATE() -- simple statement"); + connection.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + 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(); } } diff --git a/src/main/java/perfix/Agent.java b/src/main/java/perfix/Agent.java index 667e38b..30d29ff 100644 --- a/src/main/java/perfix/Agent.java +++ b/src/main/java/perfix/Agent.java @@ -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 includes = determineIncludes(); - - inst.addTransformer((classLoader, resource, aClass, protectionDomain, uninstrumentedByteCode) - -> createByteCode(includes, resource, uninstrumentedByteCode)); - } - - private static byte[] createByteCode(List 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 determineIncludes() { - return new ArrayList<>(asList(System.getProperty(INCLUDES_PROPERTY).split(","))); - } - - private static boolean shouldInclude(String resource, List 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(","))); } } diff --git a/src/main/java/perfix/ClassInstrumentor.java b/src/main/java/perfix/ClassInstrumentor.java new file mode 100644 index 0000000..0c559e7 --- /dev/null +++ b/src/main/java/perfix/ClassInstrumentor.java @@ -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 includes; + private CtClass perfixMethodInvocationClass; + private CtClass stringClass; + + ClassInstrumentor(List 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 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 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); + } +} diff --git a/src/main/java/perfix/MethodInvocation.java b/src/main/java/perfix/MethodInvocation.java index 2dea77d..c1fa759 100644 --- a/src/main/java/perfix/MethodInvocation.java +++ b/src/main/java/perfix/MethodInvocation.java @@ -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; } diff --git a/src/main/java/perfix/MutableBoolean.java b/src/main/java/perfix/MutableBoolean.java new file mode 100644 index 0000000..1d9b7f7 --- /dev/null +++ b/src/main/java/perfix/MutableBoolean.java @@ -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); + } +} diff --git a/src/main/java/perfix/server/SshSessionInstance.java b/src/main/java/perfix/server/SshSessionInstance.java index f597858..f5d9132 100644 --- a/src/main/java/perfix/server/SshSessionInstance.java +++ b/src/main/java/perfix/server/SshSessionInstance.java @@ -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; } }