From 3fbfab4288367e2ebc9ca60e5dbc70b594935ea3 Mon Sep 17 00:00:00 2001 From: hengyunabc Date: Fri, 31 Aug 2018 11:49:48 +0800 Subject: [PATCH] init --- README.md | 2 + TODO.md | 23 + agent/pom.xml | 69 + .../taobao/arthas/agent/AgentBootstrap.java | 168 +++ .../arthas/agent/ArthasClassloader.java | 36 + as-package.sh | 67 + batch.as | 3 + bin/as.bat | 90 ++ bin/as.sh | 482 +++++++ bin/install-local.sh | 35 + bin/install.sh | 48 + bin/jps.sh | 27 + client/pom.xml | 63 + .../taobao/arthas/client/TelnetConsole.java | 213 +++ .../commons/net/DatagramSocketClient.java | 316 ++++ .../commons/net/DatagramSocketFactory.java | 69 + .../net/DefaultDatagramSocketFactory.java | 79 + .../commons/net/DefaultSocketFactory.java | 230 +++ .../net/MalformedServerReplyException.java | 56 + .../commons/net/PrintCommandListener.java | 202 +++ .../commons/net/ProtocolCommandEvent.java | 148 ++ .../commons/net/ProtocolCommandListener.java | 58 + .../commons/net/ProtocolCommandSupport.java | 135 ++ .../org/apache/commons/net/SocketClient.java | 888 ++++++++++++ .../commons/net/telnet/EchoOptionHandler.java | 52 + .../telnet/InvalidTelnetOptionException.java | 62 + .../net/telnet/SimpleOptionHandler.java | 59 + .../net/telnet/SuppressGAOptionHandler.java | 52 + .../org/apache/commons/net/telnet/Telnet.java | 1265 +++++++++++++++++ .../commons/net/telnet/TelnetClient.java | 414 ++++++ .../commons/net/telnet/TelnetCommand.java | 131 ++ .../net/telnet/TelnetInputListener.java | 37 + .../commons/net/telnet/TelnetInputStream.java | 680 +++++++++ .../net/telnet/TelnetNotificationHandler.java | 68 + .../commons/net/telnet/TelnetOption.java | 193 +++ .../net/telnet/TelnetOptionHandler.java | 283 ++++ .../net/telnet/TelnetOutputStream.java | 165 +++ .../net/telnet/TerminalTypeOptionHandler.java | 112 ++ .../net/telnet/WindowSizeOptionHandler.java | 162 +++ .../apache/commons/net/util/ListenerList.java | 66 + core/pom.xml | 125 ++ .../java/com/taobao/arthas/core/Arthas.java | 93 ++ .../com/taobao/arthas/core/GlobalOptions.java | 102 ++ .../java/com/taobao/arthas/core/Option.java | 35 + .../taobao/arthas/core/advisor/Advice.java | 150 ++ .../arthas/core/advisor/AdviceListener.java | 74 + .../core/advisor/AdviceListenerAdapter.java | 43 + .../arthas/core/advisor/AdviceWeaver.java | 984 +++++++++++++ .../arthas/core/advisor/ArthasMethod.java | 98 ++ .../arthas/core/advisor/AsmCodeLock.java | 118 ++ .../taobao/arthas/core/advisor/CodeLock.java | 43 + .../taobao/arthas/core/advisor/Enhancer.java | 399 ++++++ .../arthas/core/advisor/InvokeTraceable.java | 51 + .../advisor/ReflectAdviceListenerAdapter.java | 233 +++ .../core/advisor/TracingAsmCodeLock.java | 22 + .../core/command/BuiltinCommandPack.java | 82 ++ .../taobao/arthas/core/command/Constants.java | 44 + .../core/command/ScriptSupportCommand.java | 118 ++ .../core/command/basic1000/ClsCommand.java | 17 + .../core/command/basic1000/HelpCommand.java | 88 ++ .../core/command/basic1000/KeymapCommand.java | 40 + .../core/command/basic1000/ResetCommand.java | 60 + .../command/basic1000/SessionCommand.java | 37 + .../command/basic1000/ShutdownCommand.java | 39 + .../basic1000/SystemPropertyCommand.java | 94 ++ .../command/basic1000/VersionCommand.java | 22 + .../command/express/CustomClassResolver.java | 44 + .../arthas/core/command/express/Express.java | 52 + .../command/express/ExpressException.java | 30 + .../core/command/express/ExpressFactory.java | 26 + .../core/command/express/OgnlExpress.java | 59 + .../core/command/hidden/JulyCommand.java | 105 ++ .../core/command/hidden/OptionsCommand.java | 188 +++ .../core/command/hidden/ThanksCommand.java | 24 + .../klass100/ClassDumpTransformer.java | 77 + .../command/klass100/ClassLoaderCommand.java | 555 ++++++++ .../command/klass100/DumpClassCommand.java | 141 ++ .../command/klass100/GetStaticCommand.java | 180 +++ .../core/command/klass100/JadCommand.java | 204 +++ .../command/klass100/RedefineCommand.java | 137 ++ .../command/klass100/SearchClassCommand.java | 138 ++ .../command/klass100/SearchMethodCommand.java | 160 +++ .../AbstractTraceAdviceListener.java | 95 ++ .../command/monitor200/CompleteContext.java | 53 + .../command/monitor200/DashboardCommand.java | 414 ++++++ .../monitor200/DashboardInterruptHandler.java | 25 + .../command/monitor200/EnhancerCommand.java | 310 ++++ .../monitor200/GroovyAdviceListener.java | 76 + .../monitor200/GroovyScriptCommand.java | 91 ++ .../core/command/monitor200/JvmCommand.java | 218 +++ .../monitor200/MonitorAdviceListener.java | 306 ++++ .../command/monitor200/MonitorCommand.java | 128 ++ .../monitor200/PathTraceAdviceListener.java | 13 + .../monitor200/StackAdviceListener.java | 73 + .../core/command/monitor200/StackCommand.java | 114 ++ .../command/monitor200/ThreadCommand.java | 155 ++ .../core/command/monitor200/TimeFragment.java | 33 + .../monitor200/TimeTunnelAdviceListener.java | 97 ++ .../command/monitor200/TimeTunnelCommand.java | 460 ++++++ .../command/monitor200/TimeTunnelTable.java | 221 +++ .../monitor200/TraceAdviceListener.java | 41 + .../core/command/monitor200/TraceCommand.java | 180 +++ .../core/command/monitor200/TraceEntity.java | 44 + .../monitor200/WatchAdviceListener.java | 102 ++ .../core/command/monitor200/WatchCommand.java | 180 +++ .../taobao/arthas/core/config/Configure.java | 135 ++ .../arthas/core/config/FeatureCodec.java | 243 ++++ .../arthas/core/server/ArthasBootstrap.java | 180 +++ .../com/taobao/arthas/core/shell/Shell.java | 45 + .../taobao/arthas/core/shell/ShellServer.java | 105 ++ .../arthas/core/shell/ShellServerOptions.java | 126 ++ .../arthas/core/shell/cli/CliToken.java | 23 + .../arthas/core/shell/cli/CliTokens.java | 40 + .../arthas/core/shell/cli/Completion.java | 44 + .../core/shell/cli/CompletionUtils.java | 108 ++ .../core/shell/cli/impl/CliTokenImpl.java | 127 ++ .../core/shell/command/AnnotatedCommand.java | 48 + .../arthas/core/shell/command/Command.java | 53 + .../core/shell/command/CommandBuilder.java | 61 + .../core/shell/command/CommandProcess.java | 165 +++ .../core/shell/command/CommandRegistry.java | 73 + .../core/shell/command/CommandResolver.java | 15 + .../command/impl/AnnotatedCommandImpl.java | 115 ++ .../command/impl/CommandBuilderImpl.java | 73 + .../shell/command/internal/CloseFunction.java | 11 + .../shell/command/internal/GrepHandler.java | 52 + .../command/internal/PlainTextHandler.java | 22 + .../command/internal/RedirectHandler.java | 43 + .../command/internal/StatisticsFunction.java | 13 + .../shell/command/internal/StdoutHandler.java | 55 + .../shell/command/internal/TermHandler.java | 22 + .../command/internal/WordCountHandler.java | 57 + .../arthas/core/shell/future/Future.java | 121 ++ .../core/shell/handlers/BindHandler.java | 29 + .../arthas/core/shell/handlers/Handler.java | 10 + .../core/shell/handlers/NoOpHandler.java | 20 + .../command/CommandInterruptHandler.java | 22 + .../handlers/server/SessionClosedHandler.java | 24 + .../server/SessionsClosedHandler.java | 26 + .../server/TermServerListenHandler.java | 49 + .../server/TermServerTermHandler.java | 21 + .../shell/handlers/shell/CloseHandler.java | 20 + .../CommandManagerCompletionHandler.java | 21 + .../shell/handlers/shell/FutureHandler.java | 20 + .../handlers/shell/InterruptHandler.java | 24 + .../shell/ShellForegroundUpdateHandler.java | 23 + .../handlers/shell/ShellLineHandler.java | 171 +++ .../shell/handlers/shell/SuspendHandler.java | 36 + .../handlers/term/CloseHandlerWrapper.java | 20 + .../term/DefaultTermStdinHandler.java | 22 + .../shell/handlers/term/EventHandler.java | 31 + .../shell/handlers/term/RequestHandler.java | 24 + .../handlers/term/SizeHandlerWrapper.java | 21 + .../handlers/term/StdinHandlerWrapper.java | 21 + .../shell/impl/BuiltinCommandResolver.java | 39 + .../arthas/core/shell/impl/ShellImpl.java | 179 +++ .../core/shell/impl/ShellServerImpl.java | 238 ++++ .../arthas/core/shell/session/Session.java | 110 ++ .../core/shell/session/impl/SessionImpl.java | 92 ++ .../arthas/core/shell/system/ExecStatus.java | 31 + .../taobao/arthas/core/shell/system/Job.java | 115 ++ .../core/shell/system/JobController.java | 51 + .../arthas/core/shell/system/Process.java | 178 +++ .../shell/system/impl/CommandCompletion.java | 47 + .../system/impl/GlobalJobControllerImpl.java | 109 ++ .../system/impl/InternalCommandManager.java | 147 ++ .../shell/system/impl/JobControllerImpl.java | 226 +++ .../core/shell/system/impl/JobImpl.java | 254 ++++ .../core/shell/system/impl/ProcessImpl.java | 624 ++++++++ .../arthas/core/shell/term/SignalHandler.java | 8 + .../taobao/arthas/core/shell/term/Term.java | 92 ++ .../arthas/core/shell/term/TermServer.java | 84 ++ .../taobao/arthas/core/shell/term/Tty.java | 52 + .../shell/term/impl/CompletionAdaptor.java | 73 + .../shell/term/impl/CompletionHandler.java | 32 + .../arthas/core/shell/term/impl/Helper.java | 80 ++ .../core/shell/term/impl/HttpTermServer.java | 82 ++ .../shell/term/impl/TelnetTermServer.java | 84 ++ .../arthas/core/shell/term/impl/TermImpl.java | 246 ++++ .../taobao/arthas/core/util/ArrayUtils.java | 34 + .../taobao/arthas/core/util/ArthasBanner.java | 85 ++ .../arthas/core/util/ArthasCheckUtils.java | 57 + .../taobao/arthas/core/util/Constants.java | 56 + .../taobao/arthas/core/util/DateUtils.java | 22 + .../taobao/arthas/core/util/FileUtils.java | 167 +++ .../com/taobao/arthas/core/util/IOUtils.java | 152 ++ .../com/taobao/arthas/core/util/IPUtils.java | 77 + .../com/taobao/arthas/core/util/LogUtil.java | 84 ++ .../com/taobao/arthas/core/util/NetUtils.java | 204 +++ .../taobao/arthas/core/util/ObjectUtils.java | 672 +++++++++ .../taobao/arthas/core/util/SearchUtils.java | 136 ++ .../taobao/arthas/core/util/StringUtils.java | 875 ++++++++++++ .../arthas/core/util/ThreadLocalWatch.java | 35 + .../taobao/arthas/core/util/ThreadUtil.java | 434 ++++++ .../taobao/arthas/core/util/TokenUtils.java | 48 + .../arthas/core/util/TypeRenderUtils.java | 165 +++ .../taobao/arthas/core/util/UserStatUtil.java | 112 ++ .../arthas/core/util/affect/Affect.java | 27 + .../core/util/affect/EnhancerAffect.java | 100 ++ .../arthas/core/util/affect/RowAffect.java | 45 + .../arthas/core/util/collection/GaStack.java | 18 + .../collection/ThreadUnsafeFixGaStack.java | 60 + .../util/collection/ThreadUnsafeGaStack.java | 90 ++ .../core/util/matcher/EqualsMatcher.java | 21 + .../core/util/matcher/FalseMatcher.java | 12 + .../core/util/matcher/GroupMatcher.java | 97 ++ .../arthas/core/util/matcher/Matcher.java | 17 + .../core/util/matcher/RegexMatcher.java | 21 + .../arthas/core/util/matcher/TrueMatcher.java | 13 + .../core/util/matcher/WildcardMatcher.java | 98 ++ .../arthas/core/util/metrics/RateCounter.java | 91 ++ .../core/util/metrics/SumRateCounter.java | 48 + .../core/util/reflect/ArthasReflectUtils.java | 321 +++++ .../arthas/core/util/reflect/FieldUtils.java | 451 ++++++ .../core/util/usage/StyledUsageFormatter.java | 125 ++ .../com/taobao/arthas/core/view/Ansi.java | 729 ++++++++++ .../arthas/core/view/ClassInfoView.java | 194 +++ .../com/taobao/arthas/core/view/KVView.java | 57 + .../taobao/arthas/core/view/LadderView.java | 65 + .../arthas/core/view/MethodInfoView.java | 91 ++ .../taobao/arthas/core/view/ObjectView.java | 662 +++++++++ .../taobao/arthas/core/view/TableView.java | 374 +++++ .../com/taobao/arthas/core/view/TreeView.java | 316 ++++ .../com/taobao/arthas/core/view/View.java | 14 + .../com/taobao/arthas/core/res/logo.txt | 30 + .../com/taobao/arthas/core/res/thanks.txt | 36 + .../arthas/core/shell/term/readline/inputrc | 23 + .../arthas/core/view/ObjectViewTest.java | 267 ++++ pom.xml | 100 ++ spy/pom.xml | 30 + spy/src/main/java/java/arthas/Spy.java | 96 ++ testcase/pom.xml | 34 + .../main/java/com/alibaba/arthas/Pojo.java | 31 + .../main/java/com/alibaba/arthas/Test.java | 57 + .../main/java/com/alibaba/arthas/Type.java | 5 + 235 files changed, 30452 insertions(+) create mode 100644 README.md create mode 100644 TODO.md create mode 100644 agent/pom.xml create mode 100644 agent/src/main/java/com/taobao/arthas/agent/AgentBootstrap.java create mode 100644 agent/src/main/java/com/taobao/arthas/agent/ArthasClassloader.java create mode 100755 as-package.sh create mode 100644 batch.as create mode 100644 bin/as.bat create mode 100755 bin/as.sh create mode 100644 bin/install-local.sh create mode 100644 bin/install.sh create mode 100644 bin/jps.sh create mode 100644 client/pom.xml create mode 100644 client/src/main/java/com/taobao/arthas/client/TelnetConsole.java create mode 100644 client/src/main/java/org/apache/commons/net/DatagramSocketClient.java create mode 100644 client/src/main/java/org/apache/commons/net/DatagramSocketFactory.java create mode 100644 client/src/main/java/org/apache/commons/net/DefaultDatagramSocketFactory.java create mode 100644 client/src/main/java/org/apache/commons/net/DefaultSocketFactory.java create mode 100644 client/src/main/java/org/apache/commons/net/MalformedServerReplyException.java create mode 100644 client/src/main/java/org/apache/commons/net/PrintCommandListener.java create mode 100644 client/src/main/java/org/apache/commons/net/ProtocolCommandEvent.java create mode 100644 client/src/main/java/org/apache/commons/net/ProtocolCommandListener.java create mode 100644 client/src/main/java/org/apache/commons/net/ProtocolCommandSupport.java create mode 100644 client/src/main/java/org/apache/commons/net/SocketClient.java create mode 100644 client/src/main/java/org/apache/commons/net/telnet/EchoOptionHandler.java create mode 100644 client/src/main/java/org/apache/commons/net/telnet/InvalidTelnetOptionException.java create mode 100644 client/src/main/java/org/apache/commons/net/telnet/SimpleOptionHandler.java create mode 100644 client/src/main/java/org/apache/commons/net/telnet/SuppressGAOptionHandler.java create mode 100644 client/src/main/java/org/apache/commons/net/telnet/Telnet.java create mode 100644 client/src/main/java/org/apache/commons/net/telnet/TelnetClient.java create mode 100644 client/src/main/java/org/apache/commons/net/telnet/TelnetCommand.java create mode 100644 client/src/main/java/org/apache/commons/net/telnet/TelnetInputListener.java create mode 100644 client/src/main/java/org/apache/commons/net/telnet/TelnetInputStream.java create mode 100644 client/src/main/java/org/apache/commons/net/telnet/TelnetNotificationHandler.java create mode 100644 client/src/main/java/org/apache/commons/net/telnet/TelnetOption.java create mode 100644 client/src/main/java/org/apache/commons/net/telnet/TelnetOptionHandler.java create mode 100644 client/src/main/java/org/apache/commons/net/telnet/TelnetOutputStream.java create mode 100644 client/src/main/java/org/apache/commons/net/telnet/TerminalTypeOptionHandler.java create mode 100644 client/src/main/java/org/apache/commons/net/telnet/WindowSizeOptionHandler.java create mode 100644 client/src/main/java/org/apache/commons/net/util/ListenerList.java create mode 100644 core/pom.xml create mode 100644 core/src/main/java/com/taobao/arthas/core/Arthas.java create mode 100644 core/src/main/java/com/taobao/arthas/core/GlobalOptions.java create mode 100644 core/src/main/java/com/taobao/arthas/core/Option.java create mode 100644 core/src/main/java/com/taobao/arthas/core/advisor/Advice.java create mode 100644 core/src/main/java/com/taobao/arthas/core/advisor/AdviceListener.java create mode 100644 core/src/main/java/com/taobao/arthas/core/advisor/AdviceListenerAdapter.java create mode 100644 core/src/main/java/com/taobao/arthas/core/advisor/AdviceWeaver.java create mode 100644 core/src/main/java/com/taobao/arthas/core/advisor/ArthasMethod.java create mode 100644 core/src/main/java/com/taobao/arthas/core/advisor/AsmCodeLock.java create mode 100644 core/src/main/java/com/taobao/arthas/core/advisor/CodeLock.java create mode 100644 core/src/main/java/com/taobao/arthas/core/advisor/Enhancer.java create mode 100644 core/src/main/java/com/taobao/arthas/core/advisor/InvokeTraceable.java create mode 100644 core/src/main/java/com/taobao/arthas/core/advisor/ReflectAdviceListenerAdapter.java create mode 100644 core/src/main/java/com/taobao/arthas/core/advisor/TracingAsmCodeLock.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/BuiltinCommandPack.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/Constants.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/ScriptSupportCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/basic1000/ClsCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/basic1000/HelpCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/basic1000/KeymapCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/basic1000/ResetCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/basic1000/SessionCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/basic1000/ShutdownCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/basic1000/SystemPropertyCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/basic1000/VersionCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/express/CustomClassResolver.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/express/Express.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/express/ExpressException.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/express/ExpressFactory.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/express/OgnlExpress.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/hidden/JulyCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/hidden/OptionsCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/hidden/ThanksCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/klass100/ClassDumpTransformer.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/klass100/ClassLoaderCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/klass100/DumpClassCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/klass100/GetStaticCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/klass100/JadCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/klass100/RedefineCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/klass100/SearchClassCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/klass100/SearchMethodCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/monitor200/AbstractTraceAdviceListener.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/monitor200/CompleteContext.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/monitor200/DashboardCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/monitor200/DashboardInterruptHandler.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/monitor200/EnhancerCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/monitor200/GroovyAdviceListener.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/monitor200/GroovyScriptCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/monitor200/JvmCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/monitor200/MonitorAdviceListener.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/monitor200/MonitorCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/monitor200/PathTraceAdviceListener.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/monitor200/StackAdviceListener.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/monitor200/StackCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/monitor200/ThreadCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/monitor200/TimeFragment.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/monitor200/TimeTunnelAdviceListener.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/monitor200/TimeTunnelCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/monitor200/TimeTunnelTable.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/monitor200/TraceAdviceListener.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/monitor200/TraceCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/monitor200/TraceEntity.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/monitor200/WatchAdviceListener.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/monitor200/WatchCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/config/Configure.java create mode 100644 core/src/main/java/com/taobao/arthas/core/config/FeatureCodec.java create mode 100644 core/src/main/java/com/taobao/arthas/core/server/ArthasBootstrap.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/Shell.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/ShellServer.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/ShellServerOptions.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/cli/CliToken.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/cli/CliTokens.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/cli/Completion.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/cli/CompletionUtils.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/cli/impl/CliTokenImpl.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/command/AnnotatedCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/command/Command.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/command/CommandBuilder.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/command/CommandProcess.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/command/CommandRegistry.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/command/CommandResolver.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/command/impl/AnnotatedCommandImpl.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/command/impl/CommandBuilderImpl.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/command/internal/CloseFunction.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/command/internal/GrepHandler.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/command/internal/PlainTextHandler.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/command/internal/RedirectHandler.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/command/internal/StatisticsFunction.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/command/internal/StdoutHandler.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/command/internal/TermHandler.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/command/internal/WordCountHandler.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/future/Future.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/handlers/BindHandler.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/handlers/Handler.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/handlers/NoOpHandler.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/handlers/command/CommandInterruptHandler.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/handlers/server/SessionClosedHandler.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/handlers/server/SessionsClosedHandler.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/handlers/server/TermServerListenHandler.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/handlers/server/TermServerTermHandler.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/handlers/shell/CloseHandler.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/handlers/shell/CommandManagerCompletionHandler.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/handlers/shell/FutureHandler.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/handlers/shell/InterruptHandler.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/handlers/shell/ShellForegroundUpdateHandler.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/handlers/shell/ShellLineHandler.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/handlers/shell/SuspendHandler.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/handlers/term/CloseHandlerWrapper.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/handlers/term/DefaultTermStdinHandler.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/handlers/term/EventHandler.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/handlers/term/RequestHandler.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/handlers/term/SizeHandlerWrapper.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/handlers/term/StdinHandlerWrapper.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/impl/BuiltinCommandResolver.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/impl/ShellImpl.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/impl/ShellServerImpl.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/session/Session.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/session/impl/SessionImpl.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/system/ExecStatus.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/system/Job.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/system/JobController.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/system/Process.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/system/impl/CommandCompletion.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/system/impl/GlobalJobControllerImpl.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/system/impl/InternalCommandManager.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/system/impl/JobControllerImpl.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/system/impl/JobImpl.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/system/impl/ProcessImpl.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/term/SignalHandler.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/term/Term.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/term/TermServer.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/term/Tty.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/term/impl/CompletionAdaptor.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/term/impl/CompletionHandler.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/term/impl/Helper.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/term/impl/HttpTermServer.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/term/impl/TelnetTermServer.java create mode 100644 core/src/main/java/com/taobao/arthas/core/shell/term/impl/TermImpl.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/ArrayUtils.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/ArthasBanner.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/ArthasCheckUtils.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/Constants.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/DateUtils.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/FileUtils.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/IOUtils.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/IPUtils.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/LogUtil.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/NetUtils.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/ObjectUtils.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/SearchUtils.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/StringUtils.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/ThreadLocalWatch.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/ThreadUtil.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/TokenUtils.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/TypeRenderUtils.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/UserStatUtil.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/affect/Affect.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/affect/EnhancerAffect.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/affect/RowAffect.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/collection/GaStack.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/collection/ThreadUnsafeFixGaStack.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/collection/ThreadUnsafeGaStack.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/matcher/EqualsMatcher.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/matcher/FalseMatcher.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/matcher/GroupMatcher.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/matcher/Matcher.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/matcher/RegexMatcher.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/matcher/TrueMatcher.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/matcher/WildcardMatcher.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/metrics/RateCounter.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/metrics/SumRateCounter.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/reflect/ArthasReflectUtils.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/reflect/FieldUtils.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/usage/StyledUsageFormatter.java create mode 100644 core/src/main/java/com/taobao/arthas/core/view/Ansi.java create mode 100644 core/src/main/java/com/taobao/arthas/core/view/ClassInfoView.java create mode 100644 core/src/main/java/com/taobao/arthas/core/view/KVView.java create mode 100644 core/src/main/java/com/taobao/arthas/core/view/LadderView.java create mode 100644 core/src/main/java/com/taobao/arthas/core/view/MethodInfoView.java create mode 100644 core/src/main/java/com/taobao/arthas/core/view/ObjectView.java create mode 100644 core/src/main/java/com/taobao/arthas/core/view/TableView.java create mode 100644 core/src/main/java/com/taobao/arthas/core/view/TreeView.java create mode 100644 core/src/main/java/com/taobao/arthas/core/view/View.java create mode 100644 core/src/main/resources/com/taobao/arthas/core/res/logo.txt create mode 100644 core/src/main/resources/com/taobao/arthas/core/res/thanks.txt create mode 100644 core/src/main/resources/com/taobao/arthas/core/shell/term/readline/inputrc create mode 100644 core/src/test/java/com/taobao/arthas/core/view/ObjectViewTest.java create mode 100644 pom.xml create mode 100644 spy/pom.xml create mode 100644 spy/src/main/java/java/arthas/Spy.java create mode 100644 testcase/pom.xml create mode 100644 testcase/src/main/java/com/alibaba/arthas/Pojo.java create mode 100644 testcase/src/main/java/com/alibaba/arthas/Test.java create mode 100644 testcase/src/main/java/com/alibaba/arthas/Type.java diff --git a/README.md b/README.md new file mode 100644 index 00000000..9f369166 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +在线诊断利器Arthas + diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..3c0e2cb4 --- /dev/null +++ b/TODO.md @@ -0,0 +1,23 @@ + +* 代码还是很乱,需要继续重构 +* 依赖需要清理,几个问题: + * 所有 apache 的 common 库应当不需要 + * json 库有好几份 + * `jopt-simple` 看下能不能用 `cli` 取代 + * `cli`, `termd` 的 artifactId, version 需要想下。是不是应该直接拿进来。他们的依赖也需要仔细看一下 +* termd 依赖 netty,感觉有点重,而且第一次 attach 比较慢,不确定是 netty 的问题还是 attach 的问题 +* 目前 web console 依赖 termd 中自带的 term.js 和 css,需要美化,需要想下如何集成到研发门户上 +* 因为现在没有 Java 客户端了,所以 batch mode 也就没有了 +* `com.taobao.arthas.core.shell.session.Session` 的能力需要和以前的 session 的实现对标。其中: + * 真的需要 textmode 吗?我觉得这个应该是 option 的事情 + * 真的需要 encoding 吗?我觉得仍然应该在 option 中定义,就算是真的需要,因为我觉得就应该是 UTF-8 + * duration 是应当展示的,session 的列表也许也应当展示 + * 需要仔细看下 session 过期是否符合预期 + * 多人协作的时候 session 原来是在多人之间共享的吗? +* 所有的命令现在实现的是 AnnotatedCommand,需要继续增强的是: + * Help 中的格式化输出被删除。需要为 `@Description` 定义一套统一的格式 + * 命令的输入以及输出的日志 (record logger) 被删除,需要重新实现,因为现在是用 `CommandProcess` 来输出,所以,需要在 `CommandProcess` 的实现里打日志 +* `com.taobao.arthas.core.GlobalOptions` 看上去好奇怪,感觉是 OptionCommand 应当做的事情 +* `com.taobao.arthas.core.config.Configure` 需要清理,尤其是和 http 相关的 +* 需要合并 develop 分支上后续的修复 +* 代码中的 TODO/FIXME \ No newline at end of file diff --git a/agent/pom.xml b/agent/pom.xml new file mode 100644 index 00000000..c7a9a0ad --- /dev/null +++ b/agent/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + com.taobao.arthas + arthas-all + 3.0.0-SNAPSHOT + + arthas-agent + arthas-agent + + + + com.taobao.arthas + arthas-spy + ${project.version} + provided + + + junit + junit + test + + + + + arthas-agent + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + UTF-8 + true + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + attached + + package + + + jar-with-dependencies + + + + com.taobao.arthas.agent.AgentBootstrap + com.taobao.arthas.agent.AgentBootstrap + true + true + + + + + + + + + + diff --git a/agent/src/main/java/com/taobao/arthas/agent/AgentBootstrap.java b/agent/src/main/java/com/taobao/arthas/agent/AgentBootstrap.java new file mode 100644 index 00000000..5fa6bdf0 --- /dev/null +++ b/agent/src/main/java/com/taobao/arthas/agent/AgentBootstrap.java @@ -0,0 +1,168 @@ +package com.taobao.arthas.agent; + +import java.arthas.Spy; +import java.io.*; +import java.lang.instrument.Instrumentation; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.jar.JarFile; + +/** + * 代理启动类 + * + * @author vlinux on 15/5/19. + */ +public class AgentBootstrap { + + private static final String ADVICEWEAVER = "com.taobao.arthas.core.advisor.AdviceWeaver"; + private static final String ON_BEFORE = "methodOnBegin"; + private static final String ON_RETURN = "methodOnReturnEnd"; + private static final String ON_THROWS = "methodOnThrowingEnd"; + private static final String BEFORE_INVOKE = "methodOnInvokeBeforeTracing"; + private static final String AFTER_INVOKE = "methodOnInvokeAfterTracing"; + private static final String THROW_INVOKE = "methodOnInvokeThrowTracing"; + private static final String RESET = "resetArthasClassLoader"; + private static final String ARTHAS_SPY_JAR = "arthas-spy.jar"; + private static final String ARTHAS_CONFIGURE = "com.taobao.arthas.core.config.Configure"; + private static final String ARTHAS_BOOTSTRAP = "com.taobao.arthas.core.server.ArthasBootstrap"; + private static final String TO_CONFIGURE = "toConfigure"; + private static final String GET_JAVA_PID = "getJavaPid"; + private static final String GET_INSTANCE = "getInstance"; + private static final String IS_BIND = "isBind"; + private static final String BIND = "bind"; + + private static PrintStream ps = System.err; + static { + try { + File log = new File(System.getProperty("user.home") + File.separator + "logs" + File.separator + + "arthas" + File.separator + "arthas.log"); + if (!log.exists()) { + log.getParentFile().mkdir(); + log.createNewFile(); + } + ps = new PrintStream(new FileOutputStream(log, true)); + } catch (Throwable t) { + t.printStackTrace(ps); + } + } + + // 全局持有classloader用于隔离 Arthas 实现 + private static volatile ClassLoader arthasClassLoader; + + public static void premain(String args, Instrumentation inst) { + main(args, inst); + } + + public static void agentmain(String args, Instrumentation inst) { + main(args, inst); + } + + /** + * 让下次再次启动时有机会重新加载 + */ + public synchronized static void resetArthasClassLoader() { + arthasClassLoader = null; + } + + private static ClassLoader getClassLoader(Instrumentation inst, File spyJarFile, File agentJarFile) throws Throwable { + // 将Spy添加到BootstrapClassLoader + inst.appendToBootstrapClassLoaderSearch(new JarFile(spyJarFile)); + + // 构造自定义的类加载器,尽量减少Arthas对现有工程的侵蚀 + return loadOrDefineClassLoader(agentJarFile); + } + + private static ClassLoader loadOrDefineClassLoader(File agentJar) throws Throwable { + if (arthasClassLoader == null) { + arthasClassLoader = new ArthasClassloader(new URL[]{agentJar.toURI().toURL()}); + } + return arthasClassLoader; + } + + private static void initSpy(ClassLoader classLoader) throws ClassNotFoundException, NoSuchMethodException { + Class adviceWeaverClass = classLoader.loadClass(ADVICEWEAVER); + Method onBefore = adviceWeaverClass.getMethod(ON_BEFORE, int.class, ClassLoader.class, String.class, + String.class, String.class, Object.class, Object[].class); + Method onReturn = adviceWeaverClass.getMethod(ON_RETURN, Object.class); + Method onThrows = adviceWeaverClass.getMethod(ON_THROWS, Throwable.class); + Method beforeInvoke = adviceWeaverClass.getMethod(BEFORE_INVOKE, int.class, String.class, String.class, String.class); + Method afterInvoke = adviceWeaverClass.getMethod(AFTER_INVOKE, int.class, String.class, String.class, String.class); + Method throwInvoke = adviceWeaverClass.getMethod(THROW_INVOKE, int.class, String.class, String.class, String.class); + Method reset = AgentBootstrap.class.getMethod(RESET); + Spy.initForAgentLauncher(classLoader, onBefore, onReturn, onThrows, beforeInvoke, afterInvoke, throwInvoke, reset); + } + + private static synchronized void main(final String args, final Instrumentation inst) { + try { + ps.println("Arthas server agent start..."); + // 传递的args参数分两个部分:agentJar路径和agentArgs, 分别是Agent的JAR包路径和期望传递到服务端的参数 + int index = args.indexOf(';'); + String agentJar = args.substring(0, index); + final String agentArgs = args.substring(index, args.length()); + + File agentJarFile = new File(agentJar); + if (!agentJarFile.exists()) { + ps.println("Agent jar file does not exist: " + agentJarFile); + return; + } + + File spyJarFile = new File(agentJarFile.getParentFile(), ARTHAS_SPY_JAR); + if (!spyJarFile.exists()) { + ps.println("Spy jar file does not exist: " + spyJarFile); + return; + } + + /** + * Use a dedicated thread to run the binding logic to prevent possible memory leak. #195 + */ + final ClassLoader agentLoader = getClassLoader(inst, spyJarFile, agentJarFile); + initSpy(agentLoader); + + Thread bindingThread = new Thread() { + @Override + public void run() { + try { + bind(inst, agentLoader, agentArgs); + } catch (Throwable throwable) { + throwable.printStackTrace(ps); + } + } + }; + + bindingThread.setName("arthas-binding-thread"); + bindingThread.start(); + bindingThread.join(); + } catch (Throwable t) { + t.printStackTrace(ps); + try { + if (ps != System.err) { + ps.close(); + } + } catch (Throwable tt) { + // ignore + } + throw new RuntimeException(t); + } + } + + private static void bind(Instrumentation inst, ClassLoader agentLoader, String args) throws Throwable { + Class classOfConfigure = agentLoader.loadClass(ARTHAS_CONFIGURE); + Object configure = classOfConfigure.getMethod(TO_CONFIGURE, String.class).invoke(null, args); + int javaPid = (Integer) classOfConfigure.getMethod(GET_JAVA_PID).invoke(configure); + Class bootstrapClass = agentLoader.loadClass(ARTHAS_BOOTSTRAP); + Object bootstrap = bootstrapClass.getMethod(GET_INSTANCE, int.class, Instrumentation.class).invoke(null, javaPid, inst); + boolean isBind = (Boolean) bootstrapClass.getMethod(IS_BIND).invoke(bootstrap); + if (!isBind) { + try { + ps.println("Arthas start to bind..."); + bootstrapClass.getMethod(BIND, classOfConfigure).invoke(bootstrap, configure); + ps.println("Arthas server bind success."); + return; + } catch (Exception e) { + ps.println("Arthas server port binding failed! Please check $HOME/logs/arthas/arthas.log for more details."); + throw e; + } + } + ps.println("Arthas server already bind."); + } +} diff --git a/agent/src/main/java/com/taobao/arthas/agent/ArthasClassloader.java b/agent/src/main/java/com/taobao/arthas/agent/ArthasClassloader.java new file mode 100644 index 00000000..4d94bc8e --- /dev/null +++ b/agent/src/main/java/com/taobao/arthas/agent/ArthasClassloader.java @@ -0,0 +1,36 @@ +package com.taobao.arthas.agent; + +import java.net.URL; +import java.net.URLClassLoader; + +/** + * @author beiwei30 on 09/12/2016. + */ +public class ArthasClassloader extends URLClassLoader { + public ArthasClassloader(URL[] urls) { + super(urls, ClassLoader.getSystemClassLoader().getParent()); + } + + @Override + protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + final Class loadedClass = findLoadedClass(name); + if (loadedClass != null) { + return loadedClass; + } + + // 优先从parent(SystemClassLoader)里加载系统类,避免抛出ClassNotFoundException + if (name != null && (name.startsWith("sun.") || name.startsWith("java."))) { + return super.loadClass(name, resolve); + } + try { + Class aClass = findClass(name); + if (resolve) { + resolveClass(aClass); + } + return aClass; + } catch (Exception e) { + // ignore + } + return super.loadClass(name, resolve); + } +} diff --git a/as-package.sh b/as-package.sh new file mode 100755 index 00000000..84b29023 --- /dev/null +++ b/as-package.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +CUR_VERSION="3.0" + +# arthas's target dir +ARTHAS_TARGET_DIR=$DIR/target/arthas + +# arthas's version +DATE=$(date '+%Y%m%d%H%M%S') + +ARTHAS_VERSION="${CUR_VERSION}.${DATE}" + +echo ${ARTHAS_VERSION} > $DIR/core/src/main/resources/com/taobao/arthas/core/res/version + +# define newset arthas lib home +NEWEST_ARTHAS_LIB_HOME=${HOME}/.arthas/lib/${ARTHAS_VERSION}/arthas + + +# exit shell with err_code +# $1 : err_code +# $2 : err_msg +exit_on_err() +{ + [[ ! -z "${2}" ]] && echo "${2}" 1>&2 + exit ${1} +} + +# maven package the arthas +mvn clean package -Dmaven.test.skip=true -f $DIR/pom.xml \ +|| exit_on_err 1 "package arthas failed." + +rm -r $DIR/core/src/main/resources/com/taobao/arthas/core/res/version + +# reset the target dir +mkdir -p ${ARTHAS_TARGET_DIR} + +# copy jar to TARGET_DIR +cp $DIR/spy/target/arthas-spy.jar ${ARTHAS_TARGET_DIR}/arthas-spy.jar +cp $DIR/core/target/arthas-core-jar-with-dependencies.jar ${ARTHAS_TARGET_DIR}/arthas-core.jar +cp $DIR/agent/target/arthas-agent-jar-with-dependencies.jar ${ARTHAS_TARGET_DIR}/arthas-agent.jar +cp $DIR/client/target/arthas-client-jar-with-dependencies.jar ${ARTHAS_TARGET_DIR}/arthas-client.jar + +# copy shell to TARGET_DIR +cat $DIR/bin/install-local.sh|sed "s/ARTHAS_VERSION=0.0/ARTHAS_VERSION=${ARTHAS_VERSION}/g" > ${ARTHAS_TARGET_DIR}/install-local.sh +chmod +x ${ARTHAS_TARGET_DIR}/install-local.sh +cp $DIR/bin/as.sh ${ARTHAS_TARGET_DIR}/as.sh +cp $DIR/bin/as.bat ${ARTHAS_TARGET_DIR}/as.bat + +# zip the arthas +cd $DIR/target/ +zip -r arthas-${ARTHAS_VERSION}-bin.zip arthas/ +cd - + +# install to local +mkdir -p ${NEWEST_ARTHAS_LIB_HOME} +cp $DIR/target/arthas/* ${NEWEST_ARTHAS_LIB_HOME}/ + +if [ $# -gt 0 ] && [ "$1" = "-release" ]; then + echo "creating git tag ${ARTHAS_VERSION}..." + git tag -a ${ARTHAS_VERSION} -m "release ${ARTHAS_VERSION}" + if [ $? -eq 0 ]; then + echo "A local git tag ${ARTHAS_VERSION} has been created, please use 'git tag -l' to verify it." + echo "To commit the tag to remote repo, please run 'git push --tags' manually. " + fi +fi diff --git a/batch.as b/batch.as new file mode 100644 index 00000000..bc442679 --- /dev/null +++ b/batch.as @@ -0,0 +1,3 @@ +dashboard -n 1 +sysprop +watch arthas.Test test "@com.alibaba.arthas.Test@n.entrySet().iterator.{? #this.key.name()=='STOP' }" -n 2 diff --git a/bin/as.bat b/bin/as.bat new file mode 100644 index 00000000..71b4c3a6 --- /dev/null +++ b/bin/as.bat @@ -0,0 +1,90 @@ +@echo off + +REM ---------------------------------------------------------------------------- +REM program : Arthas +REM author : Core Engine @ Taobao.com +REM date : 2015-11-11 +REM version : 3.0 +REM ---------------------------------------------------------------------------- + + + +set ERROR_CODE=0 + +:init +REM Decide how to startup depending on the version of windows + +REM -- Win98ME +if NOT "%OS%"=="Windows_NT" goto Win9xArg + +REM set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal +goto WinNTGetScriptDir + +:Win9xArg +REM Slurp the command line arguments. This loop allows for an unlimited number +REM of arguments (up to the command line limit, anyway). +set BASEDIR=%CD% +goto repoSetup + +:WinNTGetScriptDir +set BASEDIR=%~dp0 + +:repoSetup +set AGENT_JAR=%BASEDIR%\arthas-agent.jar +set CORE_JAR=%BASEDIR%\arthas-core.jar + +set PID=%1 + +REM Setup JAVA_HOME +if "%JAVA_HOME%" == "" goto noJavaHome +if not exist "%JAVA_HOME%\bin\java.exe" goto noJavaHome +if not exist "%JAVA_HOME%\lib\tools.jar" goto noJavaHome +set JAVACMD="%JAVA_HOME%\bin\java" +set BOOT_CLASSPATH="-Xbootclasspath/a:%JAVA_HOME%\lib\tools.jar" +goto okJava + +:noJavaHome +echo The JAVA_HOME environment variable is not defined correctly. +echo It is needed to run this program. +echo NB: JAVA_HOME should point to a JDK not a JRE. +goto exit + +:okJava +set JAVACMD="%JAVA_HOME%"\bin\java + +REM Reaching here means variables are defined and arguments have been captured +:endInit + +%JAVACMD% -Dfile.encoding=UTF-8 %BOOT_CLASSPATH% -jar "%CORE_JAR%" -pid "%PID%" -target-ip 127.0.0.1 -telnet-port 3658 -http-port 8563 -core "%CORE_JAR%" -agent "%AGENT_JAR%" +if %ERRORLEVEL% NEQ 0 goto error +goto attachSuccess + +:error +if "%OS%"=="Windows_NT" endlocal +set ERROR_CODE=%ERRORLEVEL% +goto endNT + +:attachSuccess +REM %JAVACMD% -Dfile.encoding=UTF-8 -Djava.awt.headless=true -cp "%CORE_JAR%" com.taobao.arthas.core.ArthasConsole 127.0.0.1 3658 +telnet 127.0.0.1 3658 + +REM set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" goto endNT + +REM For old DOS remove the set variables from ENV - we assume they were not set +REM before we started - at least we don't leave any baggage around +goto postExec + +:endNT +REM If error code is set to 1 then the endlocal was done already in :error. +if %ERROR_CODE% EQU 0 endlocal + + +:postExec + +if "%FORCE_EXIT_ON_ERROR%" == "on" ( + if %ERROR_CODE% NEQ 0 exit %ERROR_CODE% +) + +exit /B %ERROR_CODE% diff --git a/bin/as.sh b/bin/as.sh new file mode 100755 index 00000000..a3269af6 --- /dev/null +++ b/bin/as.sh @@ -0,0 +1,482 @@ +#!/bin/sh + +# program : Arthas +# author : Core Engine @ Taobao.com +# date : 2017-3-27 +# version : 3.0.3 + +# define arthas's home +ARTHAS_HOME=${HOME}/.arthas + +# define arthas's lib +ARTHAS_LIB_DIR=${ARTHAS_HOME}/lib + +# define arthas's temp dir +TMP_DIR=/tmp + +# last update arthas version +ARTHAS_VERSION= + +# current arthas script version +ARTHAS_SCRIPT_VERSION=3.0.2 + +# arthas remote url +ARTHAS_REMOTE_VERSION_URL="http://arthas.io/api/arthas/newestVersion.do" +ARTHAS_REMOTE_DOWNLOAD_URL="http://arthas.io/mdtool" + +# update timeout(sec) +SO_TIMEOUT=5 + +# define default target ip +DEFAULT_TARGET_IP="127.0.0.1" + +# define default target port +DEFAULT_TELNET_PORT="3658" +DEFAULT_HTTP_PORT="8563" + +# define JVM's OPS +JVM_OPTS="" + +# define default batch mode +BATCH_MODE=false + +# if true, the script will only attach the agent to target jvm. +ATTACH_ONLY=false + +# define batch script location +BATCH_SCRIPT= + +ARTHAS_OPTS="-Djava.awt.headless=true" + +# exit shell with err_code +# $1 : err_code +# $2 : err_msg +exit_on_err() +{ + [[ ! -z "${2}" ]] && echo "${2}" 1>&2 + exit ${1} +} + + +# get with default value +# $1 : target value +# $2 : default value +default() +{ + [[ ! -z "${1}" ]] && echo "${1}" || echo "${2}" +} + + +# check arthas permission +check_permission() +{ + [ ! -w ${HOME} ] \ + && exit_on_err 1 "permission denied, ${HOME} is not writeable." +} + + +# reset arthas work environment +# reset some options for env +reset_for_env() +{ + + # init ARTHAS' lib + mkdir -p ${ARTHAS_LIB_DIR} \ + || exit_on_err 1 "create ${ARTHAS_LIB_DIR} fail." + + # if env define the JAVA_HOME, use it first + # if is alibaba opts, use alibaba ops's default JAVA_HOME + [ -z ${JAVA_HOME} ] && JAVA_HOME=/opt/taobao/java + + # iterater throught candidates to find a proper JAVA_HOME at least contains tools.jar which is required by arthas. + if [ ! -d ${JAVA_HOME} ]; then + JAVA_HOME_CANDIDATES=($(ps aux | grep java | grep -v 'grep java' | awk '{print $11}' | sed -n 's/\/bin\/java$//p')) + for JAVA_HOME_TEMP in ${JAVA_HOME_CANDIDATES[@]}; do + if [ -f ${JAVA_HOME_TEMP}/lib/tools.jar ]; then + JAVA_HOME=${JAVA_HOME_TEMP} + break + fi + done + fi + + # check the jvm version, we need 1.6+ + local JAVA_VERSION=$(${JAVA_HOME}/bin/java -version 2>&1|awk -F '"' '$2>"1.5"{print $2}') + [[ ! -x ${JAVA_HOME} || -z ${JAVA_VERSION} ]] && exit_on_err 1 "illegal ENV, please set \$JAVA_HOME to JDK6+" + + # when java version greater than 9, there is no tools.jar + if [[ ! "$JAVA_VERSION" =~ ^9 ]];then + # check tools.jar exists + if [ ! -f ${JAVA_HOME}/lib/tools.jar ]; then + exit_on_err 1 "${JAVA_HOME}/lib/tools.jar does not exist, arthas could not be launched!" + else + BOOT_CLASSPATH=-Xbootclasspath/a:${JAVA_HOME}/lib/tools.jar + fi + fi + + # reset CHARSET for alibaba opts, we use GBK + [[ -x /opt/taobao/java ]] && JVM_OPTS="-Dinput.encoding=GBK ${JVM_OPTS} " + +} + +# get latest version from local +get_local_version() +{ + ls ${ARTHAS_LIB_DIR} | sort | tail -1 +} + +# get latest version from remote +get_remote_version() +{ + curl -sLk --connect-timeout ${SO_TIMEOUT} "${ARTHAS_REMOTE_VERSION_URL}" +} + +# make version format to comparable format like 000.000.(0){15} +# $1 : version +to_comparable_version() +{ + echo ${1}|awk -F "." '{printf("%d.%d.%d\n",$1,$2,$3)}' +} + +# update arthas if necessary +update_if_necessary() +{ + local update_version=$1 + + if [ ! -d ${ARTHAS_LIB_DIR}/${update_version} ]; then + echo "updating version ${update_version} ..." + + local temp_target_lib_dir="$TMP_DIR/temp_${update_version}_$$" + local temp_target_lib_zip="${temp_target_lib_dir}/arthas-${update_version}-bin.zip" + local target_lib_dir="${ARTHAS_LIB_DIR}/${update_version}" + + # clean + rm -rf ${temp_target_lib_dir} + rm -rf ${target_lib_dir} + + mkdir -p "${temp_target_lib_dir}" \ + || exit_on_err 1 "create ${temp_target_lib_dir} fail." + + # download current arthas version + curl \ + -#Lk \ + --connect-timeout ${SO_TIMEOUT} \ + -o ${temp_target_lib_zip} \ + "${ARTHAS_REMOTE_DOWNLOAD_URL}/${update_version}/arthas-${update_version}-bin.zip" \ + || return 1 + + # unzip arthas lib + unzip ${temp_target_lib_zip} -d ${temp_target_lib_dir} || (rm -rf ${temp_target_lib_dir} && return 1) + + # rename + mv ${temp_target_lib_dir} ${target_lib_dir} || return 1 + + # print success + echo "update completed." + fi +} + +# the usage +usage() +{ + echo " +Usage: + $0 [-b [-f SCRIPT_FILE]] [debug] [--use-version VERSION] [--attach-only] [@IP:TELNET_PORT:HTTP_PORT] + [debug] : start the agent in debug mode + : the target Java Process ID + [IP] : the target's IP + [TELNET_PORT] : the target's PORT for telnet + [HTTP_PORT] : the target's PORT for http + [-b] : batch mode, which will disable interactive process selection. + [-f] : specify the path to batch script file. + [--attach-only] : only attach the arthas agent to target jvm. + [--use-version] : use the specified arthas version to attach. + +Example: + ./as.sh + ./as.sh @[IP] + ./as.sh @[IP:PORT] + ./as.sh debug + ./as.sh -b + ./as.sh -b -f /path/to/script + ./as.sh --attach-only + ./as.sh --use-version 2.0.20161221142407 + +Here is the list of possible java process(es) to attatch: + +$(${JAVA_HOME}/bin/jps -l | grep -v sun.tools.jps.Jps) +" +} + +# parse the argument +parse_arguments() +{ + if [ "$1" = "-h" ]; then + usage + exit 0 + fi + + if [ "$1" = "-b" ]; then + BATCH_MODE=true + shift + if [ "$1" = "-f" ]; then + if [ "x$2" != "x" ] && [ -f $2 ]; then + BATCH_SCRIPT=$2 + echo "Using script file for batch mode: $BATCH_SCRIPT" + shift # -f + shift # /path/to/script + else + echo "Invalid script file $2." + return 1 + fi + fi + fi + + if [ "$1" = "debug" ] ; then + if [ -z "$JPDA_TRANSPORT" ]; then + JPDA_TRANSPORT="dt_socket" + fi + if [ -z "$JPDA_ADDRESS" ]; then + JPDA_ADDRESS="8888" + fi + if [ -z "$JPDA_SUSPEND" ]; then + JPDA_SUSPEND="n" + fi + if [ -z "$JPDA_OPTS" ]; then + JPDA_OPTS="-agentlib:jdwp=transport=$JPDA_TRANSPORT,address=$JPDA_ADDRESS,server=y,suspend=$JPDA_SUSPEND" + fi + ARTHAS_OPTS="$JPDA_OPTS $ARTHAS_OPTS" + shift + fi + + # use custom version + if [ "$1" = "--use-version" ]; then + shift + ARTHAS_VERSION=$1 + shift + fi + + # attach only mode + if [ "$1" = "--attach-only" ]; then + ATTACH_ONLY=true + shift + fi + + TARGET_PID=$(echo ${1}|awk -F "@" '{print $1}'); + TARGET_IP=$(echo ${1}|awk -F "@|:" '{print $2}'); + TELNET_PORT=$(echo ${1}|awk -F ":" '{print $2}'); + HTTP_PORT=$(echo ${1}|awk -F ":" '{print $3}'); + + # check pid + if [ -z ${TARGET_PID} ] && [ ${BATCH_MODE} = false ]; then + # interactive mode + IFS=$'\n' + CANDIDATES=($(${JAVA_HOME}/bin/jps -l | grep -v sun.tools.jps.Jps | awk '{print $0}')) + + if [ ${#CANDIDATES[@]} -eq 0 ]; then + echo "Error: no available java process to attach." + return 1 + fi + + echo "Found existing java process, please choose one and hit RETURN." + + index=0 + suggest=1 + # auto select tomcat/pandora-boot process + for process in "${CANDIDATES[@]}"; do + index=$(($index+1)) + if [ $(echo ${process} | grep -c org.apache.catalina.startup.Bootstrap) -eq 1 ] \ + || [ $(echo ${process} | grep -c com.taobao.pandora.boot.loader.SarLauncher) -eq 1 ] + then + suggest=${index} + break + fi + done + + index=0 + for process in "${CANDIDATES[@]}"; do + index=$(($index+1)) + if [ ${index} -eq ${suggest} ]; then + echo "* [$index]: ${process}" + else + echo " [$index]: ${process}" + fi + done + + read choice + + if [ -z ${choice} ]; then + choice=${suggest} + fi + + TARGET_PID=`echo ${CANDIDATES[$(($choice-1))]} | cut -d ' ' -f 1` + + elif [ -z ${TARGET_PID} ]; then + # batch mode is enabled, no interactive process selection. + echo "Illegal arguments, the is required." 1>&2 + return 1 + fi + + # reset ${ip} to default if empty + [ -z ${TARGET_IP} ] && TARGET_IP=${DEFAULT_TARGET_IP} + + # reset ${port} to default if empty + [ -z ${TELNET_PORT} ] && TELNET_PORT=${DEFAULT_TELNET_PORT} + [ -z ${HTTP_PORT} ] && HTTP_PORT=${DEFAULT_HTTP_PORT} + + return 0 + +} + + +# attach arthas to target jvm +# $1 : arthas_local_version +attach_jvm() +{ + local arthas_version=$1 + local arthas_lib_dir=${ARTHAS_LIB_DIR}/${arthas_version}/arthas + + echo "Attaching to ${TARGET_PID} using version ${1}..." + + if [ ${TARGET_IP} = ${DEFAULT_TARGET_IP} ]; then + if [[ "${arthas_version}" > "3.0" ]]; then + ${JAVA_HOME}/bin/java \ + ${ARTHAS_OPTS} ${BOOT_CLASSPATH} ${JVM_OPTS} \ + -jar ${arthas_lib_dir}/arthas-core.jar \ + -pid ${TARGET_PID} \ + -target-ip ${TARGET_IP} \ + -telnet-port ${TELNET_PORT} \ + -http-port ${HTTP_PORT} \ + -core "${arthas_lib_dir}/arthas-core.jar" \ + -agent "${arthas_lib_dir}/arthas-agent.jar" + else + # for compatibility + ${JAVA_HOME}/bin/java \ + ${ARTHAS_OPTS} ${BOOT_CLASSPATH} ${JVM_OPTS} \ + -jar ${arthas_lib_dir}/arthas-core.jar \ + -pid ${TARGET_PID} \ + -target ${TARGET_IP}":"${TELNET_PORT} \ + -core "${arthas_lib_dir}/arthas-core.jar" \ + -agent "${arthas_lib_dir}/arthas-agent.jar" + + # verify_pid + echo "help" > /tmp/command + PID=`${JAVA_HOME}/bin/java -cp ${arthas_lib_dir}/arthas-core.jar ${ARTHAS_OPTS}\ + com.taobao.arthas.core.ArthasConsole ${TARGET_IP} ${TELNET_PORT} -b -f /tmp/command \ + | grep PID | awk '{print $2}'` + rm /tmp/command + if [ ! -z ${PID} ] && [ "${PID}" != "${TARGET_PID}" ]; then + echo "WARNING: Arthas server is running on ${PID} instead of ${TARGET_PID}, exiting." + exit 1 + fi + fi + fi +} + +sanity_check() { + # 0 check whether the pid exist + local pid=$(ps -p ${TARGET_PID} -o pid=) + if [ -z ${pid} ]; then + exit_on_err 1 "The target pid (${TARGET_PID}) does not exist!" + fi + + # 1 check the current user matches the process owner + local current_user=$(id -u -n) + # the last '=' after 'user' eliminates the column header + local target_user=$(ps -p "${TARGET_PID}" -o user=) + if [ "$current_user" != "$target_user" ]; then + echo "The current user ($current_user) does not match with the owner of process ${TARGET_PID} ($target_user)." + echo "To solve this, choose one of the following command:" + echo " 1) sudo su $target_user && ./as.sh" + echo " 2) sudo -u $target_user -EH ./as.sh" + exit_on_err 1 + fi +} + +# active console +# $1 : arthas_local_version +active_console() +{ + local arthas_version=$1 + local arthas_lib_dir=${ARTHAS_LIB_DIR}/${arthas_version}/arthas + + if [[ "${arthas_version}" > "3.0" ]]; then + if [ "${BATCH_MODE}" = "true" ]; then + ${JAVA_HOME}/bin/java ${ARTHAS_OPTS} ${JVM_OPTS} \ + -jar ${arthas_lib_dir}/arthas-client.jar \ + ${TARGET_IP} \ + -p ${TELNET_PORT} \ + -f ${BATCH_SCRIPT} + elif type telnet 2>&1 >> /dev/null; then + # use telnet + telnet ${TARGET_IP} ${TELNET_PORT} + else + echo "'telnet' is required." 1>&2 + return 1 + fi + else + # for compatibility + # use default console + ARGS="${TARGET_IP} ${TELNET_PORT}" + if [ ${BATCH_MODE} = true ]; then + ARGS="$ARGS -b" + fi + if [ ! -z ${BATCH_SCRIPT} ]; then + ARGS="$ARGS -f $BATCH_SCRIPT" + fi + eval ${JAVA_HOME}/bin/java ${ARTHAS_OPTS} \ + -cp ${arthas_lib_dir}/arthas-core.jar \ + com.taobao.arthas.core.ArthasConsole \ + ${ARGS} + fi +} + +# the main +main() +{ + echo "Arthas script version: $ARTHAS_SCRIPT_VERSION" + echo "Try out Arthas online: /arthas/web-console" + + check_permission + reset_for_env + + parse_arguments "${@}" \ + || exit_on_err 1 "$(usage)" + + local remote_version=$(get_remote_version) + + if [ -z ${ARTHAS_VERSION} ]; then + update_if_necessary ${remote_version} || echo "update fail, ignore this update." 1>&2 + else + update_if_necessary ${ARTHAS_VERSION} || echo "update fail, ignore this update." 1>&2 + fi + + local arthas_local_version=$(get_local_version) + + if [ ! -z ${ARTHAS_VERSION} ]; then + arthas_local_version=${ARTHAS_VERSION} + fi + + if [ ! -d ${ARTHAS_LIB_DIR}/${arthas_local_version} ]; then + exit_on_err 1 "arthas not found, please check your network." + fi + + sanity_check + + echo "Calculating attach execution time..." + time (attach_jvm ${arthas_local_version} || exit 1) + + if [ $? -ne 0 ]; then + exit_on_err 1 "attach to target jvm (${TARGET_PID}) failed, check ${HOME}/logs/arthas/arthas.log or stderr of target jvm for any exceptions." + fi + + echo "Attach success." + + if [ ${ATTACH_ONLY} = false ]; then + echo "Connecting to arthas server... current timestamp is `date +%s`" + active_console ${arthas_local_version} + fi +} + + + +main "${@}" diff --git a/bin/install-local.sh b/bin/install-local.sh new file mode 100644 index 00000000..0d709abc --- /dev/null +++ b/bin/install-local.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# define newset arthas's version +# need ../arthas-packages.sh replace the version number +ARTHAS_VERSION=0.0 + +# define newset arthas lib home +ARTHAS_LIB_HOME=${HOME}/.arthas/lib/${ARTHAS_VERSION}/arthas + +# exit shell with err_code +# $1 : err_code +# $2 : err_msg +exit_on_err() +{ + [[ ! -z "${2}" ]] && echo "${2}" 1>&2 + exit ${1} +} + +# install to local if necessary +if [[ ! -x ${ARTHAS_LIB_HOME} ]]; then + + # install to local + mkdir -p ${ARTHAS_LIB_HOME} \ + || exit_on_err 1 "create target directory ${ARTHAS_LIB_HOME} failed." + + # copy jar files + cp *.jar ${ARTHAS_LIB_HOME}/ + + # make it -x + chmod +x ./as.sh + +fi + +echo "install to local successed." + diff --git a/bin/install.sh b/bin/install.sh new file mode 100644 index 00000000..f8524017 --- /dev/null +++ b/bin/install.sh @@ -0,0 +1,48 @@ +#! /bin/bash + +# temp file of as.sh +TEMP_ARTHAS_FILE="./as.sh.$$" + +# target file of as.sh +TARGET_ARTHAS_FILE="./as.sh" + +# update timeout(sec) +SO_TIMEOUT=60 + +# default downloading url +ARTHAS_FILE_URL="http://arthas.io/arthas/as.sh" + +# exit shell with err_code +# $1 : err_code +# $2 : err_msg +exit_on_err() +{ + [[ ! -z "${2}" ]] && echo "${2}" 1>&2 + exit ${1} +} + +# check permission to download && install +[ ! -w ./ ] && exit_on_err 1 "permission denied, target directory ./ was not writable." + +if [ $# -gt 1 ] && [ $1 = "--url" ]; then + shift + ARTHAS_FILE_URL=$1 + shift +fi + +# download from aliyunos +echo "downloading... ${TEMP_ARTHAS_FILE}" +curl \ + -sLk \ + --connect-timeout ${SO_TIMEOUT} \ + $ARTHAS_FILE_URL \ + -o ${TEMP_ARTHAS_FILE} \ +|| exit_on_err 1 "download failed!" + +# wirte or overwrite local file +rm -rf as.sh +mv ${TEMP_ARTHAS_FILE} ${TARGET_ARTHAS_FILE} +chmod +x ${TARGET_ARTHAS_FILE} + +# done +echo "Arthas install successed." diff --git a/bin/jps.sh b/bin/jps.sh new file mode 100644 index 00000000..fdc0f9b4 --- /dev/null +++ b/bin/jps.sh @@ -0,0 +1,27 @@ +#!/bin/sh + +# jps.sh version 1.0.2 + +# there might be multiple java processes, e.g. log-agent +JPS_CMDS=($(ps aux | grep java | grep -v 'grep java' | awk '{print $11}' | sed -n 's/java$/jps/p')) + +# find the first executable jps command +JPS_CMD="" +for jps in ${JPS_CMDS[@]}; do + if [ -x $jps ]; then + JPS_CMD=$jps + break + fi +done + +if [ "$JPS_CMD" == "" ]; then + echo "No Java Process Found on this Machine." + exit 1 +else + result=`$JPS_CMD -lmv | grep -v jps` + if [ "$result" == "" ]; then + ps aux | grep -E '^admin.*java.*' | grep -v grep | awk 'BEGIN{ORS=""}{print $2" ";for(j=NF;j>=12;j--){if(match($j, /^\-[a-zA-Z0-9]/)) {break;} } for(i=j+1;i<=NF;i++) {print $i" "} for(i=12;i<=j;i++) {print $i" "} print "\n" }' + else + echo "$result" + fi +fi diff --git a/client/pom.xml b/client/pom.xml new file mode 100644 index 00000000..a68a48ad --- /dev/null +++ b/client/pom.xml @@ -0,0 +1,63 @@ + + + + arthas-all + com.taobao.arthas + 3.0.0-SNAPSHOT + + 4.0.0 + + arthas-client + arthas-client + + + arthas-client + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + UTF-8 + true + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + attached + + package + + + jar-with-dependencies + + + + com.taobao.arthas.client.TelnetConsole + + + core engine team, middleware group, alibaba inc. + + + + + + + + + + + + com.alibaba.middleware + cli + + + + \ No newline at end of file diff --git a/client/src/main/java/com/taobao/arthas/client/TelnetConsole.java b/client/src/main/java/com/taobao/arthas/client/TelnetConsole.java new file mode 100644 index 00000000..e331f0dc --- /dev/null +++ b/client/src/main/java/com/taobao/arthas/client/TelnetConsole.java @@ -0,0 +1,213 @@ +package com.taobao.arthas.client; + +import com.taobao.middleware.cli.Argument; +import com.taobao.middleware.cli.CLI; +import com.taobao.middleware.cli.CLIs; +import com.taobao.middleware.cli.CommandLine; +import com.taobao.middleware.cli.Option; +import com.taobao.middleware.cli.TypedOption; +import org.apache.commons.net.telnet.TelnetClient; +import org.apache.commons.net.telnet.WindowSizeOptionHandler; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * @author ralf0131 2016-12-29 11:55. + */ +public class TelnetConsole{ + + private static final String PROMPT = "$"; + private static final String DEFAULT_TELNET_PORT = "3658"; + private static final int DEFAULT_CONNECTION_TIMEOUT = 5000; // 5000 ms + private static final String DEFAULT_WINDOW_WIDTH = "120"; + private static final String DEFAULT_WINDOW_HEIGHT = "40"; + private static final int DEFAULT_BUFFER_SIZE = 1024; + + private TelnetClient telnet; + private String address; + private int port; + private InputStream in; + private PrintStream out; + + public TelnetConsole(String address, int port, int width, int height) { + this.telnet = new TelnetClient(); + this.address = address; + this.port = port; + try { + telnet.addOptionHandler(new WindowSizeOptionHandler(width, height, true, false, true, false)); + } catch (Exception e) { + e.printStackTrace(); + } + telnet.setConnectTimeout(DEFAULT_CONNECTION_TIMEOUT); + } + + public void connect() { + try { + // Connect to the specified server + telnet.connect(address, port); + // Get input and output stream references + in = telnet.getInputStream(); + out = new PrintStream(telnet.getOutputStream()); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public String readUntil(String prompt) { + try { + StringBuilder sBuffer = new StringBuilder(); + byte[] b = new byte[DEFAULT_BUFFER_SIZE]; + while(true) { + int size = in.read(b); + if(-1 != size) { + sBuffer.append(new String(b,0,size)); + String data = sBuffer.toString(); + if(data.trim().endsWith(prompt)) { + break; + } + } + } + return sBuffer.toString(); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + public String readUntilPrompt() { + return readUntil(PROMPT); + } + + public void write(String value) { + try { + out.println(value); + out.flush(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void sendCommand(String command) { + try { + write(command); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void disconnect() { + try { + telnet.disconnect(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 批处理模式 + */ + public void batchModeRun(File batchFile) { + if (batchFile == null || !batchFile.exists()) { + return; + } + batchModeRun(readLines(batchFile)); + } + + private void batchModeRun(List commands) { + for (String command: commands) { + // send command to server + sendCommand(command + " | plaintext"); + // read result from server and output + String response = readUntilPrompt(); + System.out.print(response); + } + } + + private List readLines(File batchFile) { + List list = new ArrayList(); + BufferedReader br = null; + try { + br = new BufferedReader(new FileReader(batchFile)); + String line = br.readLine(); + while (line != null) { + list.add(line); + line = br.readLine(); + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (br != null) { + try { + br.close(); + } catch (IOException e) { + // ignore + } + } + } + return list; + } + + public static void main(String[] args) { + try { + if (args.length < 1) { + System.err.println("Usage: TelnetConsole [-p PORT] [-c COMMAND] [-f BATCH_FILE] [-w WIDTH] [-h HEIGHT]"); + System.exit(1); + } + + CommandLine commandLine = parseArguments(args); + + TelnetConsole console = new TelnetConsole( + (String)commandLine.getArgumentValue("target-ip"), + (Integer)commandLine.getOptionValue("p"), + (Integer)commandLine.getOptionValue("w"), + (Integer)commandLine.getOptionValue("h")); + + console.connect(); + String logo = console.readUntilPrompt(); + System.out.print(logo); + + String cmd = commandLine.getOptionValue("c"); + if (cmd != null) { + List cmds = new ArrayList(); + for (String c: cmd.split(";")) { + cmds.add(c.trim()); + } + console.batchModeRun(cmds); + } + + String filePath = commandLine.getOptionValue("f"); + if (filePath != null) { + File batchFile = new File(filePath); + console.batchModeRun(batchFile); + } + + console.disconnect(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static CommandLine parseArguments(String[] args) { + Argument addr = new Argument().setArgName("target-ip").setIndex(0).setRequired(true); + Option port = new TypedOption().setType(Integer.class).setShortName("p") + .setDefaultValue(DEFAULT_TELNET_PORT); + Option command = new TypedOption().setType(String.class).setShortName("c"); + Option batchFileOption = new TypedOption().setType(String.class).setShortName("f"); + Option width = new TypedOption().setType(Integer.class).setShortName("w") + .setDefaultValue(DEFAULT_WINDOW_WIDTH); + Option height = new TypedOption().setType(Integer.class).setShortName("h") + .setDefaultValue(DEFAULT_WINDOW_HEIGHT); + CLI cli = CLIs.create("TelnetConsole").addArgument(addr).addOption(port) + .addOption(command).addOption(batchFileOption).addOption(width).addOption(height); + return cli.parse(Arrays.asList(args)); + } + +} diff --git a/client/src/main/java/org/apache/commons/net/DatagramSocketClient.java b/client/src/main/java/org/apache/commons/net/DatagramSocketClient.java new file mode 100644 index 00000000..5ba059fe --- /dev/null +++ b/client/src/main/java/org/apache/commons/net/DatagramSocketClient.java @@ -0,0 +1,316 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.net; + +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketException; +import java.nio.charset.Charset; + +/*** + * The DatagramSocketClient provides the basic operations that are required + * of client objects accessing datagram sockets. It is meant to be + * subclassed to avoid having to rewrite the same code over and over again + * to open a socket, close a socket, set timeouts, etc. Of special note + * is the {@link #setDatagramSocketFactory setDatagramSocketFactory } + * method, which allows you to control the type of DatagramSocket the + * DatagramSocketClient creates for network communications. This is + * especially useful for adding things like proxy support as well as better + * support for applets. For + * example, you could create a + * {@link org.apache.commons.net.DatagramSocketFactory} + * that + * requests browser security capabilities before creating a socket. + * All classes derived from DatagramSocketClient should use the + * {@link #_socketFactory_ _socketFactory_ } member variable to + * create DatagramSocket instances rather than instantiating + * them by directly invoking a constructor. By honoring this contract + * you guarantee that a user will always be able to provide his own + * Socket implementations by substituting his own SocketFactory. + * + * + * @see DatagramSocketFactory + ***/ + +public abstract class DatagramSocketClient +{ + /*** + * The default DatagramSocketFactory shared by all DatagramSocketClient + * instances. + ***/ + private static final DatagramSocketFactory __DEFAULT_SOCKET_FACTORY = + new DefaultDatagramSocketFactory(); + + /** + * Charset to use for byte IO. + */ + private Charset charset = Charset.defaultCharset(); + + /*** The timeout to use after opening a socket. ***/ + protected int _timeout_; + + /*** The datagram socket used for the connection. ***/ + protected DatagramSocket _socket_; + + /*** + * A status variable indicating if the client's socket is currently open. + ***/ + protected boolean _isOpen_; + + /*** The datagram socket's DatagramSocketFactory. ***/ + protected DatagramSocketFactory _socketFactory_; + + /*** + * Default constructor for DatagramSocketClient. Initializes + * _socket_ to null, _timeout_ to 0, and _isOpen_ to false. + ***/ + public DatagramSocketClient() + { + _socket_ = null; + _timeout_ = 0; + _isOpen_ = false; + _socketFactory_ = __DEFAULT_SOCKET_FACTORY; + } + + + /*** + * Opens a DatagramSocket on the local host at the first available port. + * Also sets the timeout on the socket to the default timeout set + * by {@link #setDefaultTimeout setDefaultTimeout() }. + *

+ * _isOpen_ is set to true after calling this method and _socket_ + * is set to the newly opened socket. + * + * @exception SocketException If the socket could not be opened or the + * timeout could not be set. + ***/ + public void open() throws SocketException + { + _socket_ = _socketFactory_.createDatagramSocket(); + _socket_.setSoTimeout(_timeout_); + _isOpen_ = true; + } + + + /*** + * Opens a DatagramSocket on the local host at a specified port. + * Also sets the timeout on the socket to the default timeout set + * by {@link #setDefaultTimeout setDefaultTimeout() }. + *

+ * _isOpen_ is set to true after calling this method and _socket_ + * is set to the newly opened socket. + * + * @param port The port to use for the socket. + * @exception SocketException If the socket could not be opened or the + * timeout could not be set. + ***/ + public void open(int port) throws SocketException + { + _socket_ = _socketFactory_.createDatagramSocket(port); + _socket_.setSoTimeout(_timeout_); + _isOpen_ = true; + } + + + /*** + * Opens a DatagramSocket at the specified address on the local host + * at a specified port. + * Also sets the timeout on the socket to the default timeout set + * by {@link #setDefaultTimeout setDefaultTimeout() }. + *

+ * _isOpen_ is set to true after calling this method and _socket_ + * is set to the newly opened socket. + * + * @param port The port to use for the socket. + * @param laddr The local address to use. + * @exception SocketException If the socket could not be opened or the + * timeout could not be set. + ***/ + public void open(int port, InetAddress laddr) throws SocketException + { + _socket_ = _socketFactory_.createDatagramSocket(port, laddr); + _socket_.setSoTimeout(_timeout_); + _isOpen_ = true; + } + + + + /*** + * Closes the DatagramSocket used for the connection. + * You should call this method after you've finished using the class + * instance and also before you call {@link #open open() } + * again. _isOpen_ is set to false and _socket_ is set to null. + * If you call this method when the client socket is not open, + * a NullPointerException is thrown. + ***/ + public void close() + { + if (_socket_ != null) { + _socket_.close(); + } + _socket_ = null; + _isOpen_ = false; + } + + + /*** + * Returns true if the client has a currently open socket. + * + * @return True if the client has a curerntly open socket, false otherwise. + ***/ + public boolean isOpen() + { + return _isOpen_; + } + + + /*** + * Set the default timeout in milliseconds to use when opening a socket. + * After a call to open, the timeout for the socket is set using this value. + * This method should be used prior to a call to {@link #open open()} + * and should not be confused with {@link #setSoTimeout setSoTimeout()} + * which operates on the currently open socket. _timeout_ contains + * the new timeout value. + * + * @param timeout The timeout in milliseconds to use for the datagram socket + * connection. + ***/ + public void setDefaultTimeout(int timeout) + { + _timeout_ = timeout; + } + + + /*** + * Returns the default timeout in milliseconds that is used when + * opening a socket. + * + * @return The default timeout in milliseconds that is used when + * opening a socket. + ***/ + public int getDefaultTimeout() + { + return _timeout_; + } + + + /*** + * Set the timeout in milliseconds of a currently open connection. + * Only call this method after a connection has been opened + * by {@link #open open()}. + * + * @param timeout The timeout in milliseconds to use for the currently + * open datagram socket connection. + * @throws SocketException if an error setting the timeout + ***/ + public void setSoTimeout(int timeout) throws SocketException + { + _socket_.setSoTimeout(timeout); + } + + + /*** + * Returns the timeout in milliseconds of the currently opened socket. + * If you call this method when the client socket is not open, + * a NullPointerException is thrown. + * + * @return The timeout in milliseconds of the currently opened socket. + * @throws SocketException if an error getting the timeout + ***/ + public int getSoTimeout() throws SocketException + { + return _socket_.getSoTimeout(); + } + + + /*** + * Returns the port number of the open socket on the local host used + * for the connection. If you call this method when the client socket + * is not open, a NullPointerException is thrown. + * + * @return The port number of the open socket on the local host used + * for the connection. + ***/ + public int getLocalPort() + { + return _socket_.getLocalPort(); + } + + + /*** + * Returns the local address to which the client's socket is bound. + * If you call this method when the client socket is not open, a + * NullPointerException is thrown. + * + * @return The local address to which the client's socket is bound. + ***/ + public InetAddress getLocalAddress() + { + return _socket_.getLocalAddress(); + } + + + /*** + * Sets the DatagramSocketFactory used by the DatagramSocketClient + * to open DatagramSockets. If the factory value is null, then a default + * factory is used (only do this to reset the factory after having + * previously altered it). + * + * @param factory The new DatagramSocketFactory the DatagramSocketClient + * should use. + ***/ + public void setDatagramSocketFactory(DatagramSocketFactory factory) + { + if (factory == null) { + _socketFactory_ = __DEFAULT_SOCKET_FACTORY; + } else { + _socketFactory_ = factory; + } + } + + /** + * Gets the charset name. + * + * @return the charset name. + * @since 3.3 + * TODO Will be deprecated once the code requires Java 1.6 as a mininmum + */ + public String getCharsetName() { + return charset.name(); + } + + /** + * Gets the charset. + * + * @return the charset. + * @since 3.3 + */ + public Charset getCharset() { + return charset; + } + + /** + * Sets the charset. + * + * @param charset the charset. + * @since 3.3 + */ + public void setCharset(Charset charset) { + this.charset = charset; + } +} diff --git a/client/src/main/java/org/apache/commons/net/DatagramSocketFactory.java b/client/src/main/java/org/apache/commons/net/DatagramSocketFactory.java new file mode 100644 index 00000000..47c26aba --- /dev/null +++ b/client/src/main/java/org/apache/commons/net/DatagramSocketFactory.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.net; + +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketException; + +/*** + * The DatagramSocketFactory interface provides a means for the + * programmer to control the creation of datagram sockets and + * provide his own DatagramSocket implementations for use by all + * classes derived from + * {@link org.apache.commons.net.DatagramSocketClient} + * . + * This allows you to provide your own DatagramSocket implementations and + * to perform security checks or browser capability requests before + * creating a DatagramSocket. + * + * + ***/ + +public interface DatagramSocketFactory +{ + + /*** + * Creates a DatagramSocket on the local host at the first available port. + * @return the socket + * + * @exception SocketException If the socket could not be created. + ***/ + public DatagramSocket createDatagramSocket() throws SocketException; + + /*** + * Creates a DatagramSocket on the local host at a specified port. + * + * @param port The port to use for the socket. + * @return the socket + * @exception SocketException If the socket could not be created. + ***/ + public DatagramSocket createDatagramSocket(int port) throws SocketException; + + /*** + * Creates a DatagramSocket at the specified address on the local host + * at a specified port. + * + * @param port The port to use for the socket. + * @param laddr The local address to use. + * @return the socket + * @exception SocketException If the socket could not be created. + ***/ + public DatagramSocket createDatagramSocket(int port, InetAddress laddr) + throws SocketException; +} diff --git a/client/src/main/java/org/apache/commons/net/DefaultDatagramSocketFactory.java b/client/src/main/java/org/apache/commons/net/DefaultDatagramSocketFactory.java new file mode 100644 index 00000000..6f46619c --- /dev/null +++ b/client/src/main/java/org/apache/commons/net/DefaultDatagramSocketFactory.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.net; + +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketException; + +/*** + * DefaultDatagramSocketFactory implements the DatagramSocketFactory + * interface by simply wrapping the java.net.DatagramSocket + * constructors. It is the default DatagramSocketFactory used by + * {@link org.apache.commons.net.DatagramSocketClient} + * implementations. + * + * + * @see DatagramSocketFactory + * @see DatagramSocketClient + * @see DatagramSocketClient#setDatagramSocketFactory + ***/ + +public class DefaultDatagramSocketFactory implements DatagramSocketFactory +{ + + /*** + * Creates a DatagramSocket on the local host at the first available port. + * @return a new DatagramSocket + * @exception SocketException If the socket could not be created. + ***/ + @Override + public DatagramSocket createDatagramSocket() throws SocketException + { + return new DatagramSocket(); + } + + /*** + * Creates a DatagramSocket on the local host at a specified port. + * + * @param port The port to use for the socket. + * @return a new DatagramSocket + * @exception SocketException If the socket could not be created. + ***/ + @Override + public DatagramSocket createDatagramSocket(int port) throws SocketException + { + return new DatagramSocket(port); + } + + /*** + * Creates a DatagramSocket at the specified address on the local host + * at a specified port. + * + * @param port The port to use for the socket. + * @param laddr The local address to use. + * @return a new DatagramSocket + * @exception SocketException If the socket could not be created. + ***/ + @Override + public DatagramSocket createDatagramSocket(int port, InetAddress laddr) + throws SocketException + { + return new DatagramSocket(port, laddr); + } +} diff --git a/client/src/main/java/org/apache/commons/net/DefaultSocketFactory.java b/client/src/main/java/org/apache/commons/net/DefaultSocketFactory.java new file mode 100644 index 00000000..2f0d6583 --- /dev/null +++ b/client/src/main/java/org/apache/commons/net/DefaultSocketFactory.java @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.net; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.UnknownHostException; + +import javax.net.SocketFactory; + +/*** + * DefaultSocketFactory implements the SocketFactory interface by + * simply wrapping the java.net.Socket and java.net.ServerSocket + * constructors. It is the default SocketFactory used by + * {@link org.apache.commons.net.SocketClient} + * implementations. + * + * + * @see SocketFactory + * @see SocketClient + * @see SocketClient#setSocketFactory + ***/ + +public class DefaultSocketFactory extends SocketFactory +{ + /** The proxy to use when creating new sockets. */ + private final Proxy connProxy; + + /** + * The default constructor. + */ + public DefaultSocketFactory() + { + this(null); + } + + /** + * A constructor for sockets with proxy support. + * + * @param proxy The Proxy to use when creating new Sockets. + * @since 3.2 + */ + public DefaultSocketFactory(Proxy proxy) + { + connProxy = proxy; + } + + /** + * Creates an unconnected Socket. + * + * @return A new unconnected Socket. + * @exception IOException If an I/O error occurs while creating the Socket. + * @since 3.2 + */ + @Override + public Socket createSocket() throws IOException + { + if (connProxy != null) + { + return new Socket(connProxy); + } + return new Socket(); + } + + /*** + * Creates a Socket connected to the given host and port. + * + * @param host The hostname to connect to. + * @param port The port to connect to. + * @return A Socket connected to the given host and port. + * @exception UnknownHostException If the hostname cannot be resolved. + * @exception IOException If an I/O error occurs while creating the Socket. + ***/ + @Override + public Socket createSocket(String host, int port) + throws UnknownHostException, IOException + { + if (connProxy != null) + { + Socket s = new Socket(connProxy); + s.connect(new InetSocketAddress(host, port)); + return s; + } + return new Socket(host, port); + } + + /*** + * Creates a Socket connected to the given host and port. + * + * @param address The address of the host to connect to. + * @param port The port to connect to. + * @return A Socket connected to the given host and port. + * @exception IOException If an I/O error occurs while creating the Socket. + ***/ + @Override + public Socket createSocket(InetAddress address, int port) + throws IOException + { + if (connProxy != null) + { + Socket s = new Socket(connProxy); + s.connect(new InetSocketAddress(address, port)); + return s; + } + return new Socket(address, port); + } + + /*** + * Creates a Socket connected to the given host and port and + * originating from the specified local address and port. + * + * @param host The hostname to connect to. + * @param port The port to connect to. + * @param localAddr The local address to use. + * @param localPort The local port to use. + * @return A Socket connected to the given host and port. + * @exception UnknownHostException If the hostname cannot be resolved. + * @exception IOException If an I/O error occurs while creating the Socket. + ***/ + @Override + public Socket createSocket(String host, int port, + InetAddress localAddr, int localPort) + throws UnknownHostException, IOException + { + if (connProxy != null) + { + Socket s = new Socket(connProxy); + s.bind(new InetSocketAddress(localAddr, localPort)); + s.connect(new InetSocketAddress(host, port)); + return s; + } + return new Socket(host, port, localAddr, localPort); + } + + /*** + * Creates a Socket connected to the given host and port and + * originating from the specified local address and port. + * + * @param address The address of the host to connect to. + * @param port The port to connect to. + * @param localAddr The local address to use. + * @param localPort The local port to use. + * @return A Socket connected to the given host and port. + * @exception IOException If an I/O error occurs while creating the Socket. + ***/ + @Override + public Socket createSocket(InetAddress address, int port, + InetAddress localAddr, int localPort) + throws IOException + { + if (connProxy != null) + { + Socket s = new Socket(connProxy); + s.bind(new InetSocketAddress(localAddr, localPort)); + s.connect(new InetSocketAddress(address, port)); + return s; + } + return new Socket(address, port, localAddr, localPort); + } + + /*** + * Creates a ServerSocket bound to a specified port. A port + * of 0 will create the ServerSocket on a system-determined free port. + * + * @param port The port on which to listen, or 0 to use any free port. + * @return A ServerSocket that will listen on a specified port. + * @exception IOException If an I/O error occurs while creating + * the ServerSocket. + ***/ + public ServerSocket createServerSocket(int port) throws IOException + { + return new ServerSocket(port); + } + + /*** + * Creates a ServerSocket bound to a specified port with a given + * maximum queue length for incoming connections. A port of 0 will + * create the ServerSocket on a system-determined free port. + * + * @param port The port on which to listen, or 0 to use any free port. + * @param backlog The maximum length of the queue for incoming connections. + * @return A ServerSocket that will listen on a specified port. + * @exception IOException If an I/O error occurs while creating + * the ServerSocket. + ***/ + public ServerSocket createServerSocket(int port, int backlog) + throws IOException + { + return new ServerSocket(port, backlog); + } + + /*** + * Creates a ServerSocket bound to a specified port on a given local + * address with a given maximum queue length for incoming connections. + * A port of 0 will + * create the ServerSocket on a system-determined free port. + * + * @param port The port on which to listen, or 0 to use any free port. + * @param backlog The maximum length of the queue for incoming connections. + * @param bindAddr The local address to which the ServerSocket should bind. + * @return A ServerSocket that will listen on a specified port. + * @exception IOException If an I/O error occurs while creating + * the ServerSocket. + ***/ + public ServerSocket createServerSocket(int port, int backlog, + InetAddress bindAddr) + throws IOException + { + return new ServerSocket(port, backlog, bindAddr); + } +} diff --git a/client/src/main/java/org/apache/commons/net/MalformedServerReplyException.java b/client/src/main/java/org/apache/commons/net/MalformedServerReplyException.java new file mode 100644 index 00000000..e5ade4e3 --- /dev/null +++ b/client/src/main/java/org/apache/commons/net/MalformedServerReplyException.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.net; + +import java.io.IOException; + +/*** + * This exception is used to indicate that the reply from a server + * could not be interpreted. Most of the NetComponents classes attempt + * to be as lenient as possible when receiving server replies. Many + * server implementations deviate from IETF protocol specifications, making + * it necessary to be as flexible as possible. However, there will be + * certain situations where it is not possible to continue an operation + * because the server reply could not be interpreted in a meaningful manner. + * In these cases, a MalformedServerReplyException should be thrown. + * + * + ***/ + +public class MalformedServerReplyException extends IOException +{ + + private static final long serialVersionUID = 6006765264250543945L; + + /*** Constructs a MalformedServerReplyException with no message ***/ + public MalformedServerReplyException() + { + super(); + } + + /*** + * Constructs a MalformedServerReplyException with a specified message. + * + * @param message The message explaining the reason for the exception. + ***/ + public MalformedServerReplyException(String message) + { + super(message); + } + +} diff --git a/client/src/main/java/org/apache/commons/net/PrintCommandListener.java b/client/src/main/java/org/apache/commons/net/PrintCommandListener.java new file mode 100644 index 00000000..8fff4ed1 --- /dev/null +++ b/client/src/main/java/org/apache/commons/net/PrintCommandListener.java @@ -0,0 +1,202 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.net; + +import java.io.PrintStream; +import java.io.PrintWriter; + +/*** + * This is a support class for some of the example programs. It is + * a sample implementation of the ProtocolCommandListener interface + * which just prints out to a specified stream all command/reply traffic. + * + * @since 2.0 + ***/ + +public class PrintCommandListener implements ProtocolCommandListener +{ + private final PrintWriter __writer; + private final boolean __nologin; + private final char __eolMarker; + private final boolean __directionMarker; + + /** + * Create the default instance which prints everything. + * + * @param stream where to write the commands and responses + * e.g. System.out + * @since 3.0 + */ + public PrintCommandListener(PrintStream stream) + { + this(new PrintWriter(stream)); + } + + /** + * Create an instance which optionally suppresses login command text + * and indicates where the EOL starts with the specified character. + * + * @param stream where to write the commands and responses + * @param suppressLogin if {@code true}, only print command name for login + * + * @since 3.0 + */ + public PrintCommandListener(PrintStream stream, boolean suppressLogin) { + this(new PrintWriter(stream), suppressLogin); + } + + /** + * Create an instance which optionally suppresses login command text + * and indicates where the EOL starts with the specified character. + * + * @param stream where to write the commands and responses + * @param suppressLogin if {@code true}, only print command name for login + * @param eolMarker if non-zero, add a marker just before the EOL. + * + * @since 3.0 + */ + public PrintCommandListener(PrintStream stream, boolean suppressLogin, char eolMarker) { + this(new PrintWriter(stream), suppressLogin, eolMarker); + } + + /** + * Create an instance which optionally suppresses login command text + * and indicates where the EOL starts with the specified character. + * + * @param stream where to write the commands and responses + * @param suppressLogin if {@code true}, only print command name for login + * @param eolMarker if non-zero, add a marker just before the EOL. + * @param showDirection if {@code true}, add {@code "> "} or {@code "< "} as appropriate to the output + * + * @since 3.0 + */ + public PrintCommandListener(PrintStream stream, boolean suppressLogin, char eolMarker, boolean showDirection) { + this(new PrintWriter(stream), suppressLogin, eolMarker, showDirection); + } + + /** + * Create the default instance which prints everything. + * + * @param writer where to write the commands and responses + */ + public PrintCommandListener(PrintWriter writer) + { + this(writer, false); + } + + /** + * Create an instance which optionally suppresses login command text. + * + * @param writer where to write the commands and responses + * @param suppressLogin if {@code true}, only print command name for login + * + * @since 3.0 + */ + public PrintCommandListener(PrintWriter writer, boolean suppressLogin) + { + this(writer, suppressLogin, (char) 0); + } + + /** + * Create an instance which optionally suppresses login command text + * and indicates where the EOL starts with the specified character. + * + * @param writer where to write the commands and responses + * @param suppressLogin if {@code true}, only print command name for login + * @param eolMarker if non-zero, add a marker just before the EOL. + * + * @since 3.0 + */ + public PrintCommandListener(PrintWriter writer, boolean suppressLogin, char eolMarker) + { + this(writer, suppressLogin, eolMarker, false); + } + + /** + * Create an instance which optionally suppresses login command text + * and indicates where the EOL starts with the specified character. + * + * @param writer where to write the commands and responses + * @param suppressLogin if {@code true}, only print command name for login + * @param eolMarker if non-zero, add a marker just before the EOL. + * @param showDirection if {@code true}, add {@code ">} " or {@code "< "} as appropriate to the output + * + * @since 3.0 + */ + public PrintCommandListener(PrintWriter writer, boolean suppressLogin, char eolMarker, boolean showDirection) + { + __writer = writer; + __nologin = suppressLogin; + __eolMarker = eolMarker; + __directionMarker = showDirection; + } + + @Override + public void protocolCommandSent(ProtocolCommandEvent event) + { + if (__directionMarker) { + __writer.print("> "); + } + if (__nologin) { + String cmd = event.getCommand(); + if ("PASS".equalsIgnoreCase(cmd) || "USER".equalsIgnoreCase(cmd)) { + __writer.print(cmd); + __writer.println(" *******"); // Don't bother with EOL marker for this! + } else { + final String IMAP_LOGIN = "LOGIN"; + if (IMAP_LOGIN.equalsIgnoreCase(cmd)) { // IMAP + String msg = event.getMessage(); + msg=msg.substring(0, msg.indexOf(IMAP_LOGIN)+IMAP_LOGIN.length()); + __writer.print(msg); + __writer.println(" *******"); // Don't bother with EOL marker for this! + } else { + __writer.print(getPrintableString(event.getMessage())); + } + } + } else { + __writer.print(getPrintableString(event.getMessage())); + } + __writer.flush(); + } + + private String getPrintableString(String msg){ + if (__eolMarker == 0) { + return msg; + } + int pos = msg.indexOf(SocketClient.NETASCII_EOL); + if (pos > 0) { + StringBuilder sb = new StringBuilder(); + sb.append(msg.substring(0,pos)); + sb.append(__eolMarker); + sb.append(msg.substring(pos)); + return sb.toString(); + } + return msg; + } + + @Override + public void protocolReplyReceived(ProtocolCommandEvent event) + { + if (__directionMarker) { + __writer.print("< "); + } + __writer.print(event.getMessage()); + __writer.flush(); + } +} + diff --git a/client/src/main/java/org/apache/commons/net/ProtocolCommandEvent.java b/client/src/main/java/org/apache/commons/net/ProtocolCommandEvent.java new file mode 100644 index 00000000..d921c615 --- /dev/null +++ b/client/src/main/java/org/apache/commons/net/ProtocolCommandEvent.java @@ -0,0 +1,148 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.net; +import java.util.EventObject; + +/*** + * There exists a large class of IETF protocols that work by sending an + * ASCII text command and arguments to a server, and then receiving an + * ASCII text reply. For debugging and other purposes, it is extremely + * useful to log or keep track of the contents of the protocol messages. + * The ProtocolCommandEvent class coupled with the + * {@link org.apache.commons.net.ProtocolCommandListener} + * interface facilitate this process. + * + * + * @see ProtocolCommandListener + * @see ProtocolCommandSupport + ***/ + +public class ProtocolCommandEvent extends EventObject +{ + private static final long serialVersionUID = 403743538418947240L; + + private final int __replyCode; + private final boolean __isCommand; + private final String __message, __command; + + /*** + * Creates a ProtocolCommandEvent signalling a command was sent to + * the server. ProtocolCommandEvents created with this constructor + * should only be sent after a command has been sent, but before the + * reply has been received. + * + * @param source The source of the event. + * @param command The string representation of the command type sent, not + * including the arguments (e.g., "STAT" or "GET"). + * @param message The entire command string verbatim as sent to the server, + * including all arguments. + ***/ + public ProtocolCommandEvent(Object source, String command, String message) + { + super(source); + __replyCode = 0; + __message = message; + __isCommand = true; + __command = command; + } + + + /*** + * Creates a ProtocolCommandEvent signalling a reply to a command was + * received. ProtocolCommandEvents created with this constructor + * should only be sent after a complete command reply has been received + * fromt a server. + * + * @param source The source of the event. + * @param replyCode The integer code indicating the natureof the reply. + * This will be the protocol integer value for protocols + * that use integer reply codes, or the reply class constant + * corresponding to the reply for protocols like POP3 that use + * strings like OK rather than integer codes (i.e., POP3Repy.OK). + * @param message The entire reply as received from the server. + ***/ + public ProtocolCommandEvent(Object source, int replyCode, String message) + { + super(source); + __replyCode = replyCode; + __message = message; + __isCommand = false; + __command = null; + } + + /*** + * Returns the string representation of the command type sent (e.g., "STAT" + * or "GET"). If the ProtocolCommandEvent is a reply event, then null + * is returned. + * + * @return The string representation of the command type sent, or null + * if this is a reply event. + ***/ + public String getCommand() + { + return __command; + } + + + /*** + * Returns the reply code of the received server reply. Undefined if + * this is not a reply event. + * + * @return The reply code of the received server reply. Undefined if + * not a reply event. + ***/ + public int getReplyCode() + { + return __replyCode; + } + + /*** + * Returns true if the ProtocolCommandEvent was generated as a result + * of sending a command. + * + * @return true If the ProtocolCommandEvent was generated as a result + * of sending a command. False otherwise. + ***/ + public boolean isCommand() + { + return __isCommand; + } + + /*** + * Returns true if the ProtocolCommandEvent was generated as a result + * of receiving a reply. + * + * @return true If the ProtocolCommandEvent was generated as a result + * of receiving a reply. False otherwise. + ***/ + public boolean isReply() + { + return !isCommand(); + } + + /*** + * Returns the entire message sent to or received from the server. + * Includes the line terminator. + * + * @return The entire message sent to or received from the server. + ***/ + public String getMessage() + { + return __message; + } +} diff --git a/client/src/main/java/org/apache/commons/net/ProtocolCommandListener.java b/client/src/main/java/org/apache/commons/net/ProtocolCommandListener.java new file mode 100644 index 00000000..f6d4231d --- /dev/null +++ b/client/src/main/java/org/apache/commons/net/ProtocolCommandListener.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.net; +import java.util.EventListener; + +/*** + * There exists a large class of IETF protocols that work by sending an + * ASCII text command and arguments to a server, and then receiving an + * ASCII text reply. For debugging and other purposes, it is extremely + * useful to log or keep track of the contents of the protocol messages. + * The ProtocolCommandListener interface coupled with the + * {@link ProtocolCommandEvent} class facilitate this process. + *

+ * To receive ProtocolCommandEvents, you merely implement the + * ProtocolCommandListener interface and register the class as a listener + * with a ProtocolCommandEvent source such as + * {@link org.apache.commons.net.ftp.FTPClient}. + * + * + * @see ProtocolCommandEvent + * @see ProtocolCommandSupport + ***/ + +public interface ProtocolCommandListener extends EventListener +{ + + /*** + * This method is invoked by a ProtocolCommandEvent source after + * sending a protocol command to a server. + * + * @param event The ProtocolCommandEvent fired. + ***/ + public void protocolCommandSent(ProtocolCommandEvent event); + + /*** + * This method is invoked by a ProtocolCommandEvent source after + * receiving a reply from a server. + * + * @param event The ProtocolCommandEvent fired. + ***/ + public void protocolReplyReceived(ProtocolCommandEvent event); + +} diff --git a/client/src/main/java/org/apache/commons/net/ProtocolCommandSupport.java b/client/src/main/java/org/apache/commons/net/ProtocolCommandSupport.java new file mode 100644 index 00000000..95611f01 --- /dev/null +++ b/client/src/main/java/org/apache/commons/net/ProtocolCommandSupport.java @@ -0,0 +1,135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.net; + +import java.io.Serializable; +import java.util.EventListener; + +import org.apache.commons.net.util.ListenerList; + +/*** + * ProtocolCommandSupport is a convenience class for managing a list of + * ProtocolCommandListeners and firing ProtocolCommandEvents. You can + * simply delegate ProtocolCommandEvent firing and listener + * registering/unregistering tasks to this class. + * + * + * @see ProtocolCommandEvent + * @see ProtocolCommandListener + ***/ + +public class ProtocolCommandSupport implements Serializable +{ + private static final long serialVersionUID = -8017692739988399978L; + + private final Object __source; + private final ListenerList __listeners; + + /*** + * Creates a ProtocolCommandSupport instance using the indicated source + * as the source of ProtocolCommandEvents. + * + * @param source The source to use for all generated ProtocolCommandEvents. + ***/ + public ProtocolCommandSupport(Object source) + { + __listeners = new ListenerList(); + __source = source; + } + + + /*** + * Fires a ProtocolCommandEvent signalling the sending of a command to all + * registered listeners, invoking their + * {@link org.apache.commons.net.ProtocolCommandListener#protocolCommandSent protocolCommandSent() } + * methods. + * + * @param command The string representation of the command type sent, not + * including the arguments (e.g., "STAT" or "GET"). + * @param message The entire command string verbatim as sent to the server, + * including all arguments. + ***/ + public void fireCommandSent(String command, String message) + { + ProtocolCommandEvent event; + + event = new ProtocolCommandEvent(__source, command, message); + + for (EventListener listener : __listeners) + { + ((ProtocolCommandListener)listener).protocolCommandSent(event); + } + } + + /*** + * Fires a ProtocolCommandEvent signalling the reception of a command reply + * to all registered listeners, invoking their + * {@link org.apache.commons.net.ProtocolCommandListener#protocolReplyReceived protocolReplyReceived() } + * methods. + * + * @param replyCode The integer code indicating the natureof the reply. + * This will be the protocol integer value for protocols + * that use integer reply codes, or the reply class constant + * corresponding to the reply for protocols like POP3 that use + * strings like OK rather than integer codes (i.e., POP3Repy.OK). + * @param message The entire reply as received from the server. + ***/ + public void fireReplyReceived(int replyCode, String message) + { + ProtocolCommandEvent event; + event = new ProtocolCommandEvent(__source, replyCode, message); + + for (EventListener listener : __listeners) + { + ((ProtocolCommandListener)listener).protocolReplyReceived(event); + } + } + + /*** + * Adds a ProtocolCommandListener. + * + * @param listener The ProtocolCommandListener to add. + ***/ + public void addProtocolCommandListener(ProtocolCommandListener listener) + { + __listeners.addListener(listener); + } + + /*** + * Removes a ProtocolCommandListener. + * + * @param listener The ProtocolCommandListener to remove. + ***/ + public void removeProtocolCommandListener(ProtocolCommandListener listener) + { + __listeners.removeListener(listener); + } + + + /*** + * Returns the number of ProtocolCommandListeners currently registered. + * + * @return The number of ProtocolCommandListeners currently registered. + ***/ + public int getListenerCount() + { + return __listeners.getListenerCount(); + } + +} + diff --git a/client/src/main/java/org/apache/commons/net/SocketClient.java b/client/src/main/java/org/apache/commons/net/SocketClient.java new file mode 100644 index 00000000..e23b985d --- /dev/null +++ b/client/src/main/java/org/apache/commons/net/SocketClient.java @@ -0,0 +1,888 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.net; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.Socket; +import java.net.SocketException; +import java.nio.charset.Charset; + +import javax.net.ServerSocketFactory; +import javax.net.SocketFactory; + + +/** + * The SocketClient provides the basic operations that are required of + * client objects accessing sockets. It is meant to be + * subclassed to avoid having to rewrite the same code over and over again + * to open a socket, close a socket, set timeouts, etc. Of special note + * is the {@link #setSocketFactory setSocketFactory } + * method, which allows you to control the type of Socket the SocketClient + * creates for initiating network connections. This is especially useful + * for adding SSL or proxy support as well as better support for applets. For + * example, you could create a + * {@link javax.net.SocketFactory} that + * requests browser security capabilities before creating a socket. + * All classes derived from SocketClient should use the + * {@link #_socketFactory_ _socketFactory_ } member variable to + * create Socket and ServerSocket instances rather than instantiating + * them by directly invoking a constructor. By honoring this contract + * you guarantee that a user will always be able to provide his own + * Socket implementations by substituting his own SocketFactory. + * @see SocketFactory + */ +public abstract class SocketClient +{ + /** + * The end of line character sequence used by most IETF protocols. That + * is a carriage return followed by a newline: "\r\n" + */ + public static final String NETASCII_EOL = "\r\n"; + + /** The default SocketFactory shared by all SocketClient instances. */ + private static final SocketFactory __DEFAULT_SOCKET_FACTORY = + SocketFactory.getDefault(); + + /** The default {@link ServerSocketFactory} */ + private static final ServerSocketFactory __DEFAULT_SERVER_SOCKET_FACTORY = + ServerSocketFactory.getDefault(); + + /** + * A ProtocolCommandSupport object used to manage the registering of + * ProtocolCommandListeners and the firing of ProtocolCommandEvents. + */ + private ProtocolCommandSupport __commandSupport; + + /** The timeout to use after opening a socket. */ + protected int _timeout_; + + /** The socket used for the connection. */ + protected Socket _socket_; + + /** The hostname used for the connection (null = no hostname supplied). */ + protected String _hostname_; + + /** The default port the client should connect to. */ + protected int _defaultPort_; + + /** The socket's InputStream. */ + protected InputStream _input_; + + /** The socket's OutputStream. */ + protected OutputStream _output_; + + /** The socket's SocketFactory. */ + protected SocketFactory _socketFactory_; + + /** The socket's ServerSocket Factory. */ + protected ServerSocketFactory _serverSocketFactory_; + + /** The socket's connect timeout (0 = infinite timeout) */ + private static final int DEFAULT_CONNECT_TIMEOUT = 0; + protected int connectTimeout = DEFAULT_CONNECT_TIMEOUT; + + /** Hint for SO_RCVBUF size */ + private int receiveBufferSize = -1; + + /** Hint for SO_SNDBUF size */ + private int sendBufferSize = -1; + + /** The proxy to use when connecting. */ + private Proxy connProxy; + + /** + * Charset to use for byte IO. + */ + private Charset charset = Charset.defaultCharset(); + + /** + * Default constructor for SocketClient. Initializes + * _socket_ to null, _timeout_ to 0, _defaultPort to 0, + * _isConnected_ to false, charset to {@code Charset.defaultCharset()} + * and _socketFactory_ to a shared instance of + * {@link org.apache.commons.net.DefaultSocketFactory}. + */ + public SocketClient() + { + _socket_ = null; + _hostname_ = null; + _input_ = null; + _output_ = null; + _timeout_ = 0; + _defaultPort_ = 0; + _socketFactory_ = __DEFAULT_SOCKET_FACTORY; + _serverSocketFactory_ = __DEFAULT_SERVER_SOCKET_FACTORY; + } + + + /** + * Because there are so many connect() methods, the _connectAction_() + * method is provided as a means of performing some action immediately + * after establishing a connection, rather than reimplementing all + * of the connect() methods. The last action performed by every + * connect() method after opening a socket is to call this method. + *

+ * This method sets the timeout on the just opened socket to the default + * timeout set by {@link #setDefaultTimeout setDefaultTimeout() }, + * sets _input_ and _output_ to the socket's InputStream and OutputStream + * respectively, and sets _isConnected_ to true. + *

+ * Subclasses overriding this method should start by calling + * super._connectAction_() first to ensure the + * initialization of the aforementioned protected variables. + * @throws IOException (SocketException) if a problem occurs with the socket + */ + protected void _connectAction_() throws IOException + { + _socket_.setSoTimeout(_timeout_); + _input_ = _socket_.getInputStream(); + _output_ = _socket_.getOutputStream(); + } + + + /** + * Opens a Socket connected to a remote host at the specified port and + * originating from the current host at a system assigned port. + * Before returning, {@link #_connectAction_ _connectAction_() } + * is called to perform connection initialization actions. + *

+ * @param host The remote host. + * @param port The port to connect to on the remote host. + * @exception SocketException If the socket timeout could not be set. + * @exception IOException If the socket could not be opened. In most + * cases you will only want to catch IOException since SocketException is + * derived from it. + */ + public void connect(InetAddress host, int port) + throws SocketException, IOException + { + _hostname_ = null; + _socket_ = _socketFactory_.createSocket(); + if (receiveBufferSize != -1) { + _socket_.setReceiveBufferSize(receiveBufferSize); + } + if (sendBufferSize != -1) { + _socket_.setSendBufferSize(sendBufferSize); + } + _socket_.connect(new InetSocketAddress(host, port), connectTimeout); + _connectAction_(); + } + + /** + * Opens a Socket connected to a remote host at the specified port and + * originating from the current host at a system assigned port. + * Before returning, {@link #_connectAction_ _connectAction_() } + * is called to perform connection initialization actions. + *

+ * @param hostname The name of the remote host. + * @param port The port to connect to on the remote host. + * @exception SocketException If the socket timeout could not be set. + * @exception IOException If the socket could not be opened. In most + * cases you will only want to catch IOException since SocketException is + * derived from it. + * @exception java.net.UnknownHostException If the hostname cannot be resolved. + */ + public void connect(String hostname, int port) + throws SocketException, IOException + { + connect(InetAddress.getByName(hostname), port); + _hostname_ = hostname; + } + + + /** + * Opens a Socket connected to a remote host at the specified port and + * originating from the specified local address and port. + * Before returning, {@link #_connectAction_ _connectAction_() } + * is called to perform connection initialization actions. + *

+ * @param host The remote host. + * @param port The port to connect to on the remote host. + * @param localAddr The local address to use. + * @param localPort The local port to use. + * @exception SocketException If the socket timeout could not be set. + * @exception IOException If the socket could not be opened. In most + * cases you will only want to catch IOException since SocketException is + * derived from it. + */ + public void connect(InetAddress host, int port, + InetAddress localAddr, int localPort) + throws SocketException, IOException + { + _hostname_ = null; + _socket_ = _socketFactory_.createSocket(); + if (receiveBufferSize != -1) { + _socket_.setReceiveBufferSize(receiveBufferSize); + } + if (sendBufferSize != -1) { + _socket_.setSendBufferSize(sendBufferSize); + } + _socket_.bind(new InetSocketAddress(localAddr, localPort)); + _socket_.connect(new InetSocketAddress(host, port), connectTimeout); + _connectAction_(); + } + + + /** + * Opens a Socket connected to a remote host at the specified port and + * originating from the specified local address and port. + * Before returning, {@link #_connectAction_ _connectAction_() } + * is called to perform connection initialization actions. + *

+ * @param hostname The name of the remote host. + * @param port The port to connect to on the remote host. + * @param localAddr The local address to use. + * @param localPort The local port to use. + * @exception SocketException If the socket timeout could not be set. + * @exception IOException If the socket could not be opened. In most + * cases you will only want to catch IOException since SocketException is + * derived from it. + * @exception java.net.UnknownHostException If the hostname cannot be resolved. + */ + public void connect(String hostname, int port, + InetAddress localAddr, int localPort) + throws SocketException, IOException + { + connect(InetAddress.getByName(hostname), port, localAddr, localPort); + _hostname_ = hostname; + } + + + /** + * Opens a Socket connected to a remote host at the current default port + * and originating from the current host at a system assigned port. + * Before returning, {@link #_connectAction_ _connectAction_() } + * is called to perform connection initialization actions. + *

+ * @param host The remote host. + * @exception SocketException If the socket timeout could not be set. + * @exception IOException If the socket could not be opened. In most + * cases you will only want to catch IOException since SocketException is + * derived from it. + */ + public void connect(InetAddress host) throws SocketException, IOException + { + _hostname_ = null; + connect(host, _defaultPort_); + } + + + /** + * Opens a Socket connected to a remote host at the current default + * port and originating from the current host at a system assigned port. + * Before returning, {@link #_connectAction_ _connectAction_() } + * is called to perform connection initialization actions. + *

+ * @param hostname The name of the remote host. + * @exception SocketException If the socket timeout could not be set. + * @exception IOException If the socket could not be opened. In most + * cases you will only want to catch IOException since SocketException is + * derived from it. + * @exception java.net.UnknownHostException If the hostname cannot be resolved. + */ + public void connect(String hostname) throws SocketException, IOException + { + connect(hostname, _defaultPort_); + _hostname_ = hostname; + } + + + /** + * Disconnects the socket connection. + * You should call this method after you've finished using the class + * instance and also before you call + * {@link #connect connect() } + * again. _isConnected_ is set to false, _socket_ is set to null, + * _input_ is set to null, and _output_ is set to null. + *

+ * @exception IOException If there is an error closing the socket. + */ + public void disconnect() throws IOException + { + closeQuietly(_socket_); + closeQuietly(_input_); + closeQuietly(_output_); + _socket_ = null; + _hostname_ = null; + _input_ = null; + _output_ = null; + } + + private void closeQuietly(Socket socket) { + if (socket != null){ + try { + socket.close(); + } catch (IOException e) { + // Ignored + } + } + } + + private void closeQuietly(Closeable close){ + if (close != null){ + try { + close.close(); + } catch (IOException e) { + // Ignored + } + } + } + /** + * Returns true if the client is currently connected to a server. + *

+ * Delegates to {@link Socket#isConnected()} + * @return True if the client is currently connected to a server, + * false otherwise. + */ + public boolean isConnected() + { + if (_socket_ == null) { + return false; + } + + return _socket_.isConnected(); + } + + /** + * Make various checks on the socket to test if it is available for use. + * Note that the only sure test is to use it, but these checks may help + * in some cases. + * @see NET-350 + * @return {@code true} if the socket appears to be available for use + * @since 3.0 + */ + public boolean isAvailable(){ + if (isConnected()) { + try + { + if (_socket_.getInetAddress() == null) { + return false; + } + if (_socket_.getPort() == 0) { + return false; + } + if (_socket_.getRemoteSocketAddress() == null) { + return false; + } + if (_socket_.isClosed()) { + return false; + } + /* these aren't exact checks (a Socket can be half-open), + but since we usually require two-way data transfer, + we check these here too: */ + if (_socket_.isInputShutdown()) { + return false; + } + if (_socket_.isOutputShutdown()) { + return false; + } + /* ignore the result, catch exceptions: */ + _socket_.getInputStream(); + _socket_.getOutputStream(); + } + catch (IOException ioex) + { + return false; + } + return true; + } else { + return false; + } + } + + /** + * Sets the default port the SocketClient should connect to when a port + * is not specified. The {@link #_defaultPort_ _defaultPort_ } + * variable stores this value. If never set, the default port is equal + * to zero. + *

+ * @param port The default port to set. + */ + public void setDefaultPort(int port) + { + _defaultPort_ = port; + } + + /** + * Returns the current value of the default port (stored in + * {@link #_defaultPort_ _defaultPort_ }). + *

+ * @return The current value of the default port. + */ + public int getDefaultPort() + { + return _defaultPort_; + } + + + /** + * Set the default timeout in milliseconds to use when opening a socket. + * This value is only used previous to a call to + * {@link #connect connect()} + * and should not be confused with {@link #setSoTimeout setSoTimeout()} + * which operates on an the currently opened socket. _timeout_ contains + * the new timeout value. + *

+ * @param timeout The timeout in milliseconds to use for the socket + * connection. + */ + public void setDefaultTimeout(int timeout) + { + _timeout_ = timeout; + } + + + /** + * Returns the default timeout in milliseconds that is used when + * opening a socket. + *

+ * @return The default timeout in milliseconds that is used when + * opening a socket. + */ + public int getDefaultTimeout() + { + return _timeout_; + } + + + /** + * Set the timeout in milliseconds of a currently open connection. + * Only call this method after a connection has been opened + * by {@link #connect connect()}. + *

+ * To set the initial timeout, use {@link #setDefaultTimeout(int)} instead. + * + * @param timeout The timeout in milliseconds to use for the currently + * open socket connection. + * @exception SocketException If the operation fails. + * @throws NullPointerException if the socket is not currently open + */ + public void setSoTimeout(int timeout) throws SocketException + { + _socket_.setSoTimeout(timeout); + } + + + /** + * Set the underlying socket send buffer size. + *

+ * @param size The size of the buffer in bytes. + * @throws SocketException never thrown, but subclasses might want to do so + * @since 2.0 + */ + public void setSendBufferSize(int size) throws SocketException { + sendBufferSize = size; + } + + /** + * Get the current sendBuffer size + * @return the size, or -1 if not initialised + * @since 3.0 + */ + protected int getSendBufferSize(){ + return sendBufferSize; + } + + /** + * Sets the underlying socket receive buffer size. + *

+ * @param size The size of the buffer in bytes. + * @throws SocketException never (but subclasses may wish to do so) + * @since 2.0 + */ + public void setReceiveBufferSize(int size) throws SocketException { + receiveBufferSize = size; + } + + /** + * Get the current receivedBuffer size + * @return the size, or -1 if not initialised + * @since 3.0 + */ + protected int getReceiveBufferSize(){ + return receiveBufferSize; + } + + /** + * Returns the timeout in milliseconds of the currently opened socket. + *

+ * @return The timeout in milliseconds of the currently opened socket. + * @exception SocketException If the operation fails. + * @throws NullPointerException if the socket is not currently open + */ + public int getSoTimeout() throws SocketException + { + return _socket_.getSoTimeout(); + } + + /** + * Enables or disables the Nagle's algorithm (TCP_NODELAY) on the + * currently opened socket. + *

+ * @param on True if Nagle's algorithm is to be enabled, false if not. + * @exception SocketException If the operation fails. + * @throws NullPointerException if the socket is not currently open + */ + public void setTcpNoDelay(boolean on) throws SocketException + { + _socket_.setTcpNoDelay(on); + } + + + /** + * Returns true if Nagle's algorithm is enabled on the currently opened + * socket. + *

+ * @return True if Nagle's algorithm is enabled on the currently opened + * socket, false otherwise. + * @exception SocketException If the operation fails. + * @throws NullPointerException if the socket is not currently open + */ + public boolean getTcpNoDelay() throws SocketException + { + return _socket_.getTcpNoDelay(); + } + + /** + * Sets the SO_KEEPALIVE flag on the currently opened socket. + * + * From the Javadocs, the default keepalive time is 2 hours (although this is + * implementation dependent). It looks as though the Windows WSA sockets implementation + * allows a specific keepalive value to be set, although this seems not to be the case on + * other systems. + * @param keepAlive If true, keepAlive is turned on + * @throws SocketException if there is a problem with the socket + * @throws NullPointerException if the socket is not currently open + * @since 2.2 + */ + public void setKeepAlive(boolean keepAlive) throws SocketException { + _socket_.setKeepAlive(keepAlive); + } + + /** + * Returns the current value of the SO_KEEPALIVE flag on the currently opened socket. + * Delegates to {@link Socket#getKeepAlive()} + * @return True if SO_KEEPALIVE is enabled. + * @throws SocketException if there is a problem with the socket + * @throws NullPointerException if the socket is not currently open + * @since 2.2 + */ + public boolean getKeepAlive() throws SocketException { + return _socket_.getKeepAlive(); + } + + /** + * Sets the SO_LINGER timeout on the currently opened socket. + *

+ * @param on True if linger is to be enabled, false if not. + * @param val The linger timeout (in hundredths of a second?) + * @exception SocketException If the operation fails. + * @throws NullPointerException if the socket is not currently open + */ + public void setSoLinger(boolean on, int val) throws SocketException + { + _socket_.setSoLinger(on, val); + } + + + /** + * Returns the current SO_LINGER timeout of the currently opened socket. + *

+ * @return The current SO_LINGER timeout. If SO_LINGER is disabled returns + * -1. + * @exception SocketException If the operation fails. + * @throws NullPointerException if the socket is not currently open + */ + public int getSoLinger() throws SocketException + { + return _socket_.getSoLinger(); + } + + + /** + * Returns the port number of the open socket on the local host used + * for the connection. + * Delegates to {@link Socket#getLocalPort()} + *

+ * @return The port number of the open socket on the local host used + * for the connection. + * @throws NullPointerException if the socket is not currently open + */ + public int getLocalPort() + { + return _socket_.getLocalPort(); + } + + + /** + * Returns the local address to which the client's socket is bound. + * Delegates to {@link Socket#getLocalAddress()} + *

+ * @return The local address to which the client's socket is bound. + * @throws NullPointerException if the socket is not currently open + */ + public InetAddress getLocalAddress() + { + return _socket_.getLocalAddress(); + } + + /** + * Returns the port number of the remote host to which the client is + * connected. + * Delegates to {@link Socket#getPort()} + *

+ * @return The port number of the remote host to which the client is + * connected. + * @throws NullPointerException if the socket is not currently open + */ + public int getRemotePort() + { + return _socket_.getPort(); + } + + + /** + * @return The remote address to which the client is connected. + * Delegates to {@link Socket#getInetAddress()} + * @throws NullPointerException if the socket is not currently open + */ + public InetAddress getRemoteAddress() + { + return _socket_.getInetAddress(); + } + + + /** + * Verifies that the remote end of the given socket is connected to the + * the same host that the SocketClient is currently connected to. This + * is useful for doing a quick security check when a client needs to + * accept a connection from a server, such as an FTP data connection or + * a BSD R command standard error stream. + *

+ * @param socket the item to check against + * @return True if the remote hosts are the same, false if not. + */ + public boolean verifyRemote(Socket socket) + { + InetAddress host1, host2; + + host1 = socket.getInetAddress(); + host2 = getRemoteAddress(); + + return host1.equals(host2); + } + + + /** + * Sets the SocketFactory used by the SocketClient to open socket + * connections. If the factory value is null, then a default + * factory is used (only do this to reset the factory after having + * previously altered it). + * Any proxy setting is discarded. + *

+ * @param factory The new SocketFactory the SocketClient should use. + */ + public void setSocketFactory(SocketFactory factory) + { + if (factory == null) { + _socketFactory_ = __DEFAULT_SOCKET_FACTORY; + } else { + _socketFactory_ = factory; + } + // re-setting the socket factory makes the proxy setting useless, + // so set the field to null so that getProxy() doesn't return a + // Proxy that we're actually not using. + connProxy = null; + } + + /** + * Sets the ServerSocketFactory used by the SocketClient to open ServerSocket + * connections. If the factory value is null, then a default + * factory is used (only do this to reset the factory after having + * previously altered it). + *

+ * @param factory The new ServerSocketFactory the SocketClient should use. + * @since 2.0 + */ + public void setServerSocketFactory(ServerSocketFactory factory) { + if (factory == null) { + _serverSocketFactory_ = __DEFAULT_SERVER_SOCKET_FACTORY; + } else { + _serverSocketFactory_ = factory; + } + } + + /** + * Sets the connection timeout in milliseconds, which will be passed to the {@link Socket} object's + * connect() method. + * @param connectTimeout The connection timeout to use (in ms) + * @since 2.0 + */ + public void setConnectTimeout(int connectTimeout) { + this.connectTimeout = connectTimeout; + } + + /** + * Get the underlying socket connection timeout. + * @return timeout (in ms) + * @since 2.0 + */ + public int getConnectTimeout() { + return connectTimeout; + } + + /** + * Get the underlying {@link ServerSocketFactory} + * @return The server socket factory + * @since 2.2 + */ + public ServerSocketFactory getServerSocketFactory() { + return _serverSocketFactory_; + } + + + /** + * Adds a ProtocolCommandListener. + * + * @param listener The ProtocolCommandListener to add. + * @since 3.0 + */ + public void addProtocolCommandListener(ProtocolCommandListener listener) { + getCommandSupport().addProtocolCommandListener(listener); + } + + /** + * Removes a ProtocolCommandListener. + * + * @param listener The ProtocolCommandListener to remove. + * @since 3.0 + */ + public void removeProtocolCommandListener(ProtocolCommandListener listener) { + getCommandSupport().removeProtocolCommandListener(listener); + } + + /** + * If there are any listeners, send them the reply details. + * + * @param replyCode the code extracted from the reply + * @param reply the full reply text + * @since 3.0 + */ + protected void fireReplyReceived(int replyCode, String reply) { + if (getCommandSupport().getListenerCount() > 0) { + getCommandSupport().fireReplyReceived(replyCode, reply); + } + } + + /** + * If there are any listeners, send them the command details. + * + * @param command the command name + * @param message the complete message, including command name + * @since 3.0 + */ + protected void fireCommandSent(String command, String message) { + if (getCommandSupport().getListenerCount() > 0) { + getCommandSupport().fireCommandSent(command, message); + } + } + + /** + * Create the CommandSupport instance if required + */ + protected void createCommandSupport(){ + __commandSupport = new ProtocolCommandSupport(this); + } + + /** + * Subclasses can override this if they need to provide their own + * instance field for backwards compatibilty. + * + * @return the CommandSupport instance, may be {@code null} + * @since 3.0 + */ + protected ProtocolCommandSupport getCommandSupport() { + return __commandSupport; + } + + /** + * Sets the proxy for use with all the connections. + * The proxy is used for connections established after the + * call to this method. + * + * @param proxy the new proxy for connections. + * @since 3.2 + */ + public void setProxy(Proxy proxy) { + setSocketFactory(new DefaultSocketFactory(proxy)); + connProxy = proxy; + } + + /** + * Gets the proxy for use with all the connections. + * @return the current proxy for connections. + */ + public Proxy getProxy() { + return connProxy; + } + + /** + * Gets the charset name. + * + * @return the charset. + * @since 3.3 + * @deprecated Since the code now requires Java 1.6 as a mininmum + */ + @Deprecated + public String getCharsetName() { + return charset.name(); + } + + /** + * Gets the charset. + * + * @return the charset. + * @since 3.3 + */ + public Charset getCharset() { + return charset; + } + + /** + * Sets the charset. + * + * @param charset the charset. + * @since 3.3 + */ + public void setCharset(Charset charset) { + this.charset = charset; + } + + /* + * N.B. Fields cannot be pulled up into a super-class without breaking binary compatibility, + * so the abstract method is needed to pass the instance to the methods which were moved here. + */ +} + + diff --git a/client/src/main/java/org/apache/commons/net/telnet/EchoOptionHandler.java b/client/src/main/java/org/apache/commons/net/telnet/EchoOptionHandler.java new file mode 100644 index 00000000..a48f59c2 --- /dev/null +++ b/client/src/main/java/org/apache/commons/net/telnet/EchoOptionHandler.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.net.telnet; + +/*** + * Implements the telnet echo option RFC 857. + ***/ +public class EchoOptionHandler extends TelnetOptionHandler +{ + /*** + * Constructor for the EchoOptionHandler. Allows defining desired + * initial setting for local/remote activation of this option and + * behaviour in case a local/remote activation request for this + * option is received. + *

+ * @param initlocal - if set to true, a WILL is sent upon connection. + * @param initremote - if set to true, a DO is sent upon connection. + * @param acceptlocal - if set to true, any DO request is accepted. + * @param acceptremote - if set to true, any WILL request is accepted. + ***/ + public EchoOptionHandler(boolean initlocal, boolean initremote, + boolean acceptlocal, boolean acceptremote) + { + super(TelnetOption.ECHO, initlocal, initremote, + acceptlocal, acceptremote); + } + + /*** + * Constructor for the EchoOptionHandler. Initial and accept + * behaviour flags are set to false + ***/ + public EchoOptionHandler() + { + super(TelnetOption.ECHO, false, false, false, false); + } + +} diff --git a/client/src/main/java/org/apache/commons/net/telnet/InvalidTelnetOptionException.java b/client/src/main/java/org/apache/commons/net/telnet/InvalidTelnetOptionException.java new file mode 100644 index 00000000..f5954403 --- /dev/null +++ b/client/src/main/java/org/apache/commons/net/telnet/InvalidTelnetOptionException.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.net.telnet; + +/*** + * The InvalidTelnetOptionException is the exception that is + * thrown whenever a TelnetOptionHandler with an invlaid + * option code is registered in TelnetClient with addOptionHandler. + ***/ +public class InvalidTelnetOptionException extends Exception +{ + + private static final long serialVersionUID = -2516777155928793597L; + + /*** + * Option code + ***/ + private final int optionCode; + + /*** + * Error message + ***/ + private final String msg; + + /*** + * Constructor for the exception. + *

+ * @param message - Error message. + * @param optcode - Option code. + ***/ + public InvalidTelnetOptionException(String message, int optcode) + { + optionCode = optcode; + msg = message; + } + + /*** + * Gets the error message of ths exception. + *

+ * @return the error message. + ***/ + @Override + public String getMessage() + { + return (msg + ": " + optionCode); + } +} diff --git a/client/src/main/java/org/apache/commons/net/telnet/SimpleOptionHandler.java b/client/src/main/java/org/apache/commons/net/telnet/SimpleOptionHandler.java new file mode 100644 index 00000000..8a88e0f2 --- /dev/null +++ b/client/src/main/java/org/apache/commons/net/telnet/SimpleOptionHandler.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.net.telnet; + +/*** + * Simple option handler that can be used for options + * that don't require subnegotiation. + ***/ +public class SimpleOptionHandler extends TelnetOptionHandler +{ + /*** + * Constructor for the SimpleOptionHandler. Allows defining desired + * initial setting for local/remote activation of this option and + * behaviour in case a local/remote activation request for this + * option is received. + *

+ * @param optcode - option code. + * @param initlocal - if set to true, a WILL is sent upon connection. + * @param initremote - if set to true, a DO is sent upon connection. + * @param acceptlocal - if set to true, any DO request is accepted. + * @param acceptremote - if set to true, any WILL request is accepted. + ***/ + public SimpleOptionHandler(int optcode, + boolean initlocal, + boolean initremote, + boolean acceptlocal, + boolean acceptremote) + { + super(optcode, initlocal, initremote, + acceptlocal, acceptremote); + } + + /*** + * Constructor for the SimpleOptionHandler. Initial and accept + * behaviour flags are set to false + *

+ * @param optcode - option code. + ***/ + public SimpleOptionHandler(int optcode) + { + super(optcode, false, false, false, false); + } + +} diff --git a/client/src/main/java/org/apache/commons/net/telnet/SuppressGAOptionHandler.java b/client/src/main/java/org/apache/commons/net/telnet/SuppressGAOptionHandler.java new file mode 100644 index 00000000..b353d481 --- /dev/null +++ b/client/src/main/java/org/apache/commons/net/telnet/SuppressGAOptionHandler.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.net.telnet; + +/*** + * Implements the telnet suppress go ahead option RFC 858. + ***/ +public class SuppressGAOptionHandler extends TelnetOptionHandler +{ + /*** + * Constructor for the SuppressGAOptionHandler. Allows defining desired + * initial setting for local/remote activation of this option and + * behaviour in case a local/remote activation request for this + * option is received. + *

+ * @param initlocal - if set to true, a WILL is sent upon connection. + * @param initremote - if set to true, a DO is sent upon connection. + * @param acceptlocal - if set to true, any DO request is accepted. + * @param acceptremote - if set to true, any WILL request is accepted. + ***/ + public SuppressGAOptionHandler(boolean initlocal, boolean initremote, + boolean acceptlocal, boolean acceptremote) + { + super(TelnetOption.SUPPRESS_GO_AHEAD, initlocal, initremote, + acceptlocal, acceptremote); + } + + /*** + * Constructor for the SuppressGAOptionHandler. Initial and accept + * behaviour flags are set to false + ***/ + public SuppressGAOptionHandler() + { + super(TelnetOption.SUPPRESS_GO_AHEAD, false, false, false, false); + } + +} diff --git a/client/src/main/java/org/apache/commons/net/telnet/Telnet.java b/client/src/main/java/org/apache/commons/net/telnet/Telnet.java new file mode 100644 index 00000000..c66f891d --- /dev/null +++ b/client/src/main/java/org/apache/commons/net/telnet/Telnet.java @@ -0,0 +1,1265 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.net.telnet; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.OutputStream; +import java.io.IOException; +import java.util.Arrays; + +import org.apache.commons.net.SocketClient; + +class Telnet extends SocketClient +{ + static final boolean debug = /*true;*/ false; + + static final boolean debugoptions = /*true;*/ false; + + static final byte[] _COMMAND_DO = { + (byte)TelnetCommand.IAC, (byte)TelnetCommand.DO + }; + + static final byte[] _COMMAND_DONT = { + (byte)TelnetCommand.IAC, (byte)TelnetCommand.DONT + }; + + static final byte[] _COMMAND_WILL = { + (byte)TelnetCommand.IAC, (byte)TelnetCommand.WILL + }; + + static final byte[] _COMMAND_WONT = { + (byte)TelnetCommand.IAC, (byte)TelnetCommand.WONT + }; + + static final byte[] _COMMAND_SB = { + (byte)TelnetCommand.IAC, (byte)TelnetCommand.SB + }; + + static final byte[] _COMMAND_SE = { + (byte)TelnetCommand.IAC, (byte)TelnetCommand.SE + }; + + static final int _WILL_MASK = 0x01, _DO_MASK = 0x02, + _REQUESTED_WILL_MASK = 0x04, _REQUESTED_DO_MASK = 0x08; + + /* public */ + static final int DEFAULT_PORT = 23; + + int[] _doResponse, _willResponse, _options; + + /* TERMINAL-TYPE option (start)*/ + /*** + * Terminal type option + ***/ + protected static final int TERMINAL_TYPE = 24; + + /*** + * Send (for subnegotiation) + ***/ + protected static final int TERMINAL_TYPE_SEND = 1; + + /*** + * Is (for subnegotiation) + ***/ + protected static final int TERMINAL_TYPE_IS = 0; + + /*** + * Is sequence (for subnegotiation) + ***/ + static final byte[] _COMMAND_IS = { + (byte) TERMINAL_TYPE, (byte) TERMINAL_TYPE_IS + }; + + /*** + * Terminal type + ***/ + private String terminalType = null; + /* TERMINAL-TYPE option (end)*/ + + /* open TelnetOptionHandler functionality (start)*/ + /*** + * Array of option handlers + ***/ + private final TelnetOptionHandler optionHandlers[]; + + /* open TelnetOptionHandler functionality (end)*/ + + /* Code Section added for supporting AYT (start)*/ + /*** + * AYT sequence + ***/ + static final byte[] _COMMAND_AYT = { + (byte) TelnetCommand.IAC, (byte) TelnetCommand.AYT + }; + + /*** + * monitor to wait for AYT + ***/ + private final Object aytMonitor = new Object(); + + /*** + * flag for AYT + ***/ + private volatile boolean aytFlag = true; + /* Code Section added for supporting AYT (end)*/ + + /*** + * The stream on which to spy + ***/ + private volatile OutputStream spyStream = null; + + /*** + * The notification handler + ***/ + private TelnetNotificationHandler __notifhand = null; + /*** + * Empty Constructor + ***/ + Telnet() + { + setDefaultPort(DEFAULT_PORT); + _doResponse = new int[TelnetOption.MAX_OPTION_VALUE + 1]; + _willResponse = new int[TelnetOption.MAX_OPTION_VALUE + 1]; + _options = new int[TelnetOption.MAX_OPTION_VALUE + 1]; + optionHandlers = + new TelnetOptionHandler[TelnetOption.MAX_OPTION_VALUE + 1]; + } + + /* TERMINAL-TYPE option (start)*/ + /*** + * This constructor lets you specify the terminal type. + * + * @param termtype - terminal type to be negotiated (ej. VT100) + ***/ + Telnet(String termtype) + { + setDefaultPort(DEFAULT_PORT); + _doResponse = new int[TelnetOption.MAX_OPTION_VALUE + 1]; + _willResponse = new int[TelnetOption.MAX_OPTION_VALUE + 1]; + _options = new int[TelnetOption.MAX_OPTION_VALUE + 1]; + terminalType = termtype; + optionHandlers = + new TelnetOptionHandler[TelnetOption.MAX_OPTION_VALUE + 1]; + } + /* TERMINAL-TYPE option (end)*/ + + /*** + * Looks for the state of the option. + * + * @return returns true if a will has been acknowledged + * + * @param option - option code to be looked up. + ***/ + boolean _stateIsWill(int option) + { + return ((_options[option] & _WILL_MASK) != 0); + } + + /*** + * Looks for the state of the option. + * + * @return returns true if a wont has been acknowledged + * + * @param option - option code to be looked up. + ***/ + boolean _stateIsWont(int option) + { + return !_stateIsWill(option); + } + + /*** + * Looks for the state of the option. + * + * @return returns true if a do has been acknowledged + * + * @param option - option code to be looked up. + ***/ + boolean _stateIsDo(int option) + { + return ((_options[option] & _DO_MASK) != 0); + } + + /*** + * Looks for the state of the option. + * + * @return returns true if a dont has been acknowledged + * + * @param option - option code to be looked up. + ***/ + boolean _stateIsDont(int option) + { + return !_stateIsDo(option); + } + + /*** + * Looks for the state of the option. + * + * @return returns true if a will has been reuqested + * + * @param option - option code to be looked up. + ***/ + boolean _requestedWill(int option) + { + return ((_options[option] & _REQUESTED_WILL_MASK) != 0); + } + + /*** + * Looks for the state of the option. + * + * @return returns true if a wont has been reuqested + * + * @param option - option code to be looked up. + ***/ + boolean _requestedWont(int option) + { + return !_requestedWill(option); + } + + /*** + * Looks for the state of the option. + * + * @return returns true if a do has been reuqested + * + * @param option - option code to be looked up. + ***/ + boolean _requestedDo(int option) + { + return ((_options[option] & _REQUESTED_DO_MASK) != 0); + } + + /*** + * Looks for the state of the option. + * + * @return returns true if a dont has been reuqested + * + * @param option - option code to be looked up. + ***/ + boolean _requestedDont(int option) + { + return !_requestedDo(option); + } + + /*** + * Sets the state of the option. + * + * @param option - option code to be set. + * @throws IOException + ***/ + void _setWill(int option) throws IOException + { + _options[option] |= _WILL_MASK; + + /* open TelnetOptionHandler functionality (start)*/ + if (_requestedWill(option)) + { + if (optionHandlers[option] != null) + { + optionHandlers[option].setWill(true); + + int subneg[] = + optionHandlers[option].startSubnegotiationLocal(); + + if (subneg != null) + { + _sendSubnegotiation(subneg); + } + } + } + /* open TelnetOptionHandler functionality (end)*/ + } + + /*** + * Sets the state of the option. + * + * @param option - option code to be set. + * @throws IOException + ***/ + void _setDo(int option) throws IOException + { + _options[option] |= _DO_MASK; + + /* open TelnetOptionHandler functionality (start)*/ + if (_requestedDo(option)) + { + if (optionHandlers[option] != null) + { + optionHandlers[option].setDo(true); + + int subneg[] = + optionHandlers[option].startSubnegotiationRemote(); + + if (subneg != null) + { + _sendSubnegotiation(subneg); + } + } + } + /* open TelnetOptionHandler functionality (end)*/ + } + + /*** + * Sets the state of the option. + * + * @param option - option code to be set. + ***/ + void _setWantWill(int option) + { + _options[option] |= _REQUESTED_WILL_MASK; + } + + /*** + * Sets the state of the option. + * + * @param option - option code to be set. + ***/ + void _setWantDo(int option) + { + _options[option] |= _REQUESTED_DO_MASK; + } + + /*** + * Sets the state of the option. + * + * @param option - option code to be set. + ***/ + void _setWont(int option) + { + _options[option] &= ~_WILL_MASK; + + /* open TelnetOptionHandler functionality (start)*/ + if (optionHandlers[option] != null) + { + optionHandlers[option].setWill(false); + } + /* open TelnetOptionHandler functionality (end)*/ + } + + /*** + * Sets the state of the option. + * + * @param option - option code to be set. + ***/ + void _setDont(int option) + { + _options[option] &= ~_DO_MASK; + + /* open TelnetOptionHandler functionality (start)*/ + if (optionHandlers[option] != null) + { + optionHandlers[option].setDo(false); + } + /* open TelnetOptionHandler functionality (end)*/ + } + + /*** + * Sets the state of the option. + * + * @param option - option code to be set. + ***/ + void _setWantWont(int option) + { + _options[option] &= ~_REQUESTED_WILL_MASK; + } + + /*** + * Sets the state of the option. + * + * @param option - option code to be set. + ***/ + void _setWantDont(int option) + { + _options[option] &= ~_REQUESTED_DO_MASK; + } + + /** + * Processes a COMMAND. + * + * @param command - option code to be set. + **/ + void _processCommand(int command) + { + if (debugoptions) + { + System.err.println("RECEIVED COMMAND: " + command); + } + + if (__notifhand != null) + { + __notifhand.receivedNegotiation( + TelnetNotificationHandler.RECEIVED_COMMAND, command); + } + } + + /** + * Processes a DO request. + * + * @param option - option code to be set. + * @throws IOException - Exception in I/O. + **/ + void _processDo(int option) throws IOException + { + if (debugoptions) + { + System.err.println("RECEIVED DO: " + + TelnetOption.getOption(option)); + } + + if (__notifhand != null) + { + __notifhand.receivedNegotiation( + TelnetNotificationHandler.RECEIVED_DO, + option); + } + + boolean acceptNewState = false; + + + /* open TelnetOptionHandler functionality (start)*/ + if (optionHandlers[option] != null) + { + acceptNewState = optionHandlers[option].getAcceptLocal(); + } + else + { + /* open TelnetOptionHandler functionality (end)*/ + /* TERMINAL-TYPE option (start)*/ + if (option == TERMINAL_TYPE) + { + if ((terminalType != null) && (terminalType.length() > 0)) + { + acceptNewState = true; + } + } + /* TERMINAL-TYPE option (end)*/ + /* open TelnetOptionHandler functionality (start)*/ + } + /* open TelnetOptionHandler functionality (end)*/ + + if (_willResponse[option] > 0) + { + --_willResponse[option]; + if (_willResponse[option] > 0 && _stateIsWill(option)) + { + --_willResponse[option]; + } + } + + if (_willResponse[option] == 0) + { + if (_requestedWont(option)) + { + + switch (option) + { + + default: + break; + + } + + + if (acceptNewState) + { + _setWantWill(option); + _sendWill(option); + } + else + { + ++_willResponse[option]; + _sendWont(option); + } + } + else + { + // Other end has acknowledged option. + + switch (option) + { + + default: + break; + + } + + } + } + + _setWill(option); + } + + /** + * Processes a DONT request. + * + * @param option - option code to be set. + * @throws IOException - Exception in I/O. + **/ + void _processDont(int option) throws IOException + { + if (debugoptions) + { + System.err.println("RECEIVED DONT: " + + TelnetOption.getOption(option)); + } + if (__notifhand != null) + { + __notifhand.receivedNegotiation( + TelnetNotificationHandler.RECEIVED_DONT, + option); + } + if (_willResponse[option] > 0) + { + --_willResponse[option]; + if (_willResponse[option] > 0 && _stateIsWont(option)) + { + --_willResponse[option]; + } + } + + if (_willResponse[option] == 0 && _requestedWill(option)) + { + + switch (option) + { + + default: + break; + + } + + /* FIX for a BUG in the negotiation (start)*/ + if ((_stateIsWill(option)) || (_requestedWill(option))) + { + _sendWont(option); + } + + _setWantWont(option); + /* FIX for a BUG in the negotiation (end)*/ + } + + _setWont(option); + } + + + /** + * Processes a WILL request. + * + * @param option - option code to be set. + * @throws IOException - Exception in I/O. + **/ + void _processWill(int option) throws IOException + { + if (debugoptions) + { + System.err.println("RECEIVED WILL: " + + TelnetOption.getOption(option)); + } + + if (__notifhand != null) + { + __notifhand.receivedNegotiation( + TelnetNotificationHandler.RECEIVED_WILL, + option); + } + + boolean acceptNewState = false; + + /* open TelnetOptionHandler functionality (start)*/ + if (optionHandlers[option] != null) + { + acceptNewState = optionHandlers[option].getAcceptRemote(); + } + /* open TelnetOptionHandler functionality (end)*/ + + if (_doResponse[option] > 0) + { + --_doResponse[option]; + if (_doResponse[option] > 0 && _stateIsDo(option)) + { + --_doResponse[option]; + } + } + + if (_doResponse[option] == 0 && _requestedDont(option)) + { + + switch (option) + { + + default: + break; + + } + + + if (acceptNewState) + { + _setWantDo(option); + _sendDo(option); + } + else + { + ++_doResponse[option]; + _sendDont(option); + } + } + + _setDo(option); + } + + /** + * Processes a WONT request. + * + * @param option - option code to be set. + * @throws IOException - Exception in I/O. + **/ + void _processWont(int option) throws IOException + { + if (debugoptions) + { + System.err.println("RECEIVED WONT: " + + TelnetOption.getOption(option)); + } + + if (__notifhand != null) + { + __notifhand.receivedNegotiation( + TelnetNotificationHandler.RECEIVED_WONT, + option); + } + + if (_doResponse[option] > 0) + { + --_doResponse[option]; + if (_doResponse[option] > 0 && _stateIsDont(option)) + { + --_doResponse[option]; + } + } + + if (_doResponse[option] == 0 && _requestedDo(option)) + { + + switch (option) + { + + default: + break; + + } + + /* FIX for a BUG in the negotiation (start)*/ + if ((_stateIsDo(option)) || (_requestedDo(option))) + { + _sendDont(option); + } + + _setWantDont(option); + /* FIX for a BUG in the negotiation (end)*/ + } + + _setDont(option); + } + + /* TERMINAL-TYPE option (start)*/ + /** + * Processes a suboption negotiation. + * + * @param suboption - subnegotiation data received + * @param suboptionLength - length of data received + * @throws IOException - Exception in I/O. + **/ + void _processSuboption(int suboption[], int suboptionLength) + throws IOException + { + if (debug) + { + System.err.println("PROCESS SUBOPTION."); + } + + /* open TelnetOptionHandler functionality (start)*/ + if (suboptionLength > 0) + { + if (optionHandlers[suboption[0]] != null) + { + int responseSuboption[] = + optionHandlers[suboption[0]].answerSubnegotiation(suboption, + suboptionLength); + _sendSubnegotiation(responseSuboption); + } + else + { + if (suboptionLength > 1) + { + if (debug) + { + for (int ii = 0; ii < suboptionLength; ii++) + { + System.err.println("SUB[" + ii + "]: " + + suboption[ii]); + } + } + if ((suboption[0] == TERMINAL_TYPE) + && (suboption[1] == TERMINAL_TYPE_SEND)) + { + _sendTerminalType(); + } + } + } + } + /* open TelnetOptionHandler functionality (end)*/ + } + + /*** + * Sends terminal type information. + * + * @throws IOException - Exception in I/O. + ***/ + final synchronized void _sendTerminalType() + throws IOException + { + if (debug) + { + System.err.println("SEND TERMINAL-TYPE: " + terminalType); + } + if (terminalType != null) + { + _output_.write(_COMMAND_SB); + _output_.write(_COMMAND_IS); + _output_.write(terminalType.getBytes(getCharset())); + _output_.write(_COMMAND_SE); + _output_.flush(); + } + } + + /* TERMINAL-TYPE option (end)*/ + + /* open TelnetOptionHandler functionality (start)*/ + /** + * Manages subnegotiation for Terminal Type. + * + * @param subn - subnegotiation data to be sent + * @throws IOException - Exception in I/O. + **/ + final synchronized void _sendSubnegotiation(int subn[]) + throws IOException + { + if (debug) + { + System.err.println("SEND SUBNEGOTIATION: "); + if (subn != null) + { + System.err.println(Arrays.toString(subn)); + } + } + if (subn != null) + { + _output_.write(_COMMAND_SB); + // Note _output_ is buffered, so might as well simplify by writing single bytes + for (int element : subn) + { + byte b = (byte) element; + if (b == (byte) TelnetCommand.IAC) { // cast is necessary because IAC is outside the signed byte range + _output_.write(b); // double any IAC bytes + } + _output_.write(b); + } + _output_.write(_COMMAND_SE); + + /* Code Section added for sending the negotiation ASAP (start)*/ + _output_.flush(); + /* Code Section added for sending the negotiation ASAP (end)*/ + } + } + /* open TelnetOptionHandler functionality (end)*/ + + /** + * Sends a command, automatically adds IAC prefix and flushes the output. + * + * @param cmd - command data to be sent + * @throws IOException - Exception in I/O. + * @since 3.0 + */ + final synchronized void _sendCommand(byte cmd) throws IOException + { + _output_.write(TelnetCommand.IAC); + _output_.write(cmd); + _output_.flush(); + } + + /* Code Section added for supporting AYT (start)*/ + /*** + * Processes the response of an AYT + ***/ + final synchronized void _processAYTResponse() + { + if (!aytFlag) + { + synchronized (aytMonitor) + { + aytFlag = true; + aytMonitor.notifyAll(); + } + } + } + /* Code Section added for supporting AYT (end)*/ + + /*** + * Called upon connection. + * + * @throws IOException - Exception in I/O. + ***/ + @Override + protected void _connectAction_() throws IOException + { + /* (start). BUGFIX: clean the option info for each connection*/ + for (int ii = 0; ii < TelnetOption.MAX_OPTION_VALUE + 1; ii++) + { + _doResponse[ii] = 0; + _willResponse[ii] = 0; + _options[ii] = 0; + if (optionHandlers[ii] != null) + { + optionHandlers[ii].setDo(false); + optionHandlers[ii].setWill(false); + } + } + /* (end). BUGFIX: clean the option info for each connection*/ + + super._connectAction_(); + _input_ = new BufferedInputStream(_input_); + _output_ = new BufferedOutputStream(_output_); + + /* open TelnetOptionHandler functionality (start)*/ + for (int ii = 0; ii < TelnetOption.MAX_OPTION_VALUE + 1; ii++) + { + if (optionHandlers[ii] != null) + { + if (optionHandlers[ii].getInitLocal()) + { + _requestWill(optionHandlers[ii].getOptionCode()); + } + + if (optionHandlers[ii].getInitRemote()) + { + _requestDo(optionHandlers[ii].getOptionCode()); + } + } + } + /* open TelnetOptionHandler functionality (end)*/ + } + + /** + * Sends a DO. + * + * @param option - Option code. + * @throws IOException - Exception in I/O. + **/ + final synchronized void _sendDo(int option) + throws IOException + { + if (debug || debugoptions) + { + System.err.println("DO: " + TelnetOption.getOption(option)); + } + _output_.write(_COMMAND_DO); + _output_.write(option); + + /* Code Section added for sending the negotiation ASAP (start)*/ + _output_.flush(); + /* Code Section added for sending the negotiation ASAP (end)*/ + } + + /** + * Requests a DO. + * + * @param option - Option code. + * @throws IOException - Exception in I/O. + **/ + final synchronized void _requestDo(int option) + throws IOException + { + if ((_doResponse[option] == 0 && _stateIsDo(option)) + || _requestedDo(option)) + { + return ; + } + _setWantDo(option); + ++_doResponse[option]; + _sendDo(option); + } + + /** + * Sends a DONT. + * + * @param option - Option code. + * @throws IOException - Exception in I/O. + **/ + final synchronized void _sendDont(int option) + throws IOException + { + if (debug || debugoptions) + { + System.err.println("DONT: " + TelnetOption.getOption(option)); + } + _output_.write(_COMMAND_DONT); + _output_.write(option); + + /* Code Section added for sending the negotiation ASAP (start)*/ + _output_.flush(); + /* Code Section added for sending the negotiation ASAP (end)*/ + } + + /** + * Requests a DONT. + * + * @param option - Option code. + * @throws IOException - Exception in I/O. + **/ + final synchronized void _requestDont(int option) + throws IOException + { + if ((_doResponse[option] == 0 && _stateIsDont(option)) + || _requestedDont(option)) + { + return ; + } + _setWantDont(option); + ++_doResponse[option]; + _sendDont(option); + } + + + /** + * Sends a WILL. + * + * @param option - Option code. + * @throws IOException - Exception in I/O. + **/ + final synchronized void _sendWill(int option) + throws IOException + { + if (debug || debugoptions) + { + System.err.println("WILL: " + TelnetOption.getOption(option)); + } + _output_.write(_COMMAND_WILL); + _output_.write(option); + + /* Code Section added for sending the negotiation ASAP (start)*/ + _output_.flush(); + /* Code Section added for sending the negotiation ASAP (end)*/ + } + + /** + * Requests a WILL. + * + * @param option - Option code. + * @throws IOException - Exception in I/O. + **/ + final synchronized void _requestWill(int option) + throws IOException + { + if ((_willResponse[option] == 0 && _stateIsWill(option)) + || _requestedWill(option)) + { + return ; + } + _setWantWill(option); + ++_doResponse[option]; + _sendWill(option); + } + + /** + * Sends a WONT. + * + * @param option - Option code. + * @throws IOException - Exception in I/O. + **/ + final synchronized void _sendWont(int option) + throws IOException + { + if (debug || debugoptions) + { + System.err.println("WONT: " + TelnetOption.getOption(option)); + } + _output_.write(_COMMAND_WONT); + _output_.write(option); + + /* Code Section added for sending the negotiation ASAP (start)*/ + _output_.flush(); + /* Code Section added for sending the negotiation ASAP (end)*/ + } + + /** + * Requests a WONT. + * + * @param option - Option code. + * @throws IOException - Exception in I/O. + **/ + final synchronized void _requestWont(int option) + throws IOException + { + if ((_willResponse[option] == 0 && _stateIsWont(option)) + || _requestedWont(option)) + { + return ; + } + _setWantWont(option); + ++_doResponse[option]; + _sendWont(option); + } + + /** + * Sends a byte. + * + * @param b - byte to send + * @throws IOException - Exception in I/O. + **/ + final synchronized void _sendByte(int b) + throws IOException + { + _output_.write(b); + + /* Code Section added for supporting spystreams (start)*/ + _spyWrite(b); + /* Code Section added for supporting spystreams (end)*/ + + } + + /* Code Section added for supporting AYT (start)*/ + /** + * Sends an Are You There sequence and waits for the result. + * + * @param timeout - Time to wait for a response (millis.) + * @throws IOException - Exception in I/O. + * @throws IllegalArgumentException - Illegal argument + * @throws InterruptedException - Interrupted during wait. + * @return true if AYT received a response, false otherwise + **/ + final boolean _sendAYT(long timeout) + throws IOException, IllegalArgumentException, InterruptedException + { + boolean retValue = false; + synchronized (aytMonitor) + { + synchronized (this) + { + aytFlag = false; + _output_.write(_COMMAND_AYT); + _output_.flush(); + } + aytMonitor.wait(timeout); + if (aytFlag == false) + { + retValue = false; + aytFlag = true; + } + else + { + retValue = true; + } + } + + return (retValue); + } + /* Code Section added for supporting AYT (end)*/ + + /* open TelnetOptionHandler functionality (start)*/ + + /** + * Registers a new TelnetOptionHandler for this telnet to use. + * + * @param opthand - option handler to be registered. + * @throws InvalidTelnetOptionException - The option code is invalid. + * @throws IOException on error + **/ + void addOptionHandler(TelnetOptionHandler opthand) + throws InvalidTelnetOptionException, IOException + { + int optcode = opthand.getOptionCode(); + if (TelnetOption.isValidOption(optcode)) + { + if (optionHandlers[optcode] == null) + { + optionHandlers[optcode] = opthand; + if (isConnected()) + { + if (opthand.getInitLocal()) + { + _requestWill(optcode); + } + + if (opthand.getInitRemote()) + { + _requestDo(optcode); + } + } + } + else + { + throw (new InvalidTelnetOptionException( + "Already registered option", optcode)); + } + } + else + { + throw (new InvalidTelnetOptionException( + "Invalid Option Code", optcode)); + } + } + + /** + * Unregisters a TelnetOptionHandler. + * + * @param optcode - Code of the option to be unregistered. + * @throws InvalidTelnetOptionException - The option code is invalid. + * @throws IOException on error + **/ + void deleteOptionHandler(int optcode) + throws InvalidTelnetOptionException, IOException + { + if (TelnetOption.isValidOption(optcode)) + { + if (optionHandlers[optcode] == null) + { + throw (new InvalidTelnetOptionException( + "Unregistered option", optcode)); + } + else + { + TelnetOptionHandler opthand = optionHandlers[optcode]; + optionHandlers[optcode] = null; + + if (opthand.getWill()) + { + _requestWont(optcode); + } + + if (opthand.getDo()) + { + _requestDont(optcode); + } + } + } + else + { + throw (new InvalidTelnetOptionException( + "Invalid Option Code", optcode)); + } + } + /* open TelnetOptionHandler functionality (end)*/ + + /* Code Section added for supporting spystreams (start)*/ + /*** + * Registers an OutputStream for spying what's going on in + * the Telnet session. + * + * @param spystream - OutputStream on which session activity + * will be echoed. + ***/ + void _registerSpyStream(OutputStream spystream) + { + spyStream = spystream; + } + + /*** + * Stops spying this Telnet. + * + ***/ + void _stopSpyStream() + { + spyStream = null; + } + + /*** + * Sends a read char on the spy stream. + * + * @param ch - character read from the session + ***/ + void _spyRead(int ch) + { + OutputStream spy = spyStream; + if (spy != null) + { + try + { + if (ch != '\r') // never write '\r' on its own + { + if (ch == '\n') + { + spy.write('\r'); // add '\r' before '\n' + } + spy.write(ch); // write original character + spy.flush(); + } + } + catch (IOException e) + { + spyStream = null; + } + } + } + + /*** + * Sends a written char on the spy stream. + * + * @param ch - character written to the session + ***/ + void _spyWrite(int ch) + { + if (!(_stateIsDo(TelnetOption.ECHO) + && _requestedDo(TelnetOption.ECHO))) + { + OutputStream spy = spyStream; + if (spy != null) + { + try + { + spy.write(ch); + spy.flush(); + } + catch (IOException e) + { + spyStream = null; + } + } + } + } + /* Code Section added for supporting spystreams (end)*/ + + /*** + * Registers a notification handler to which will be sent + * notifications of received telnet option negotiation commands. + * + * @param notifhand - TelnetNotificationHandler to be registered + ***/ + public void registerNotifHandler(TelnetNotificationHandler notifhand) + { + __notifhand = notifhand; + } + + /*** + * Unregisters the current notification handler. + * + ***/ + public void unregisterNotifHandler() + { + __notifhand = null; + } +} diff --git a/client/src/main/java/org/apache/commons/net/telnet/TelnetClient.java b/client/src/main/java/org/apache/commons/net/telnet/TelnetClient.java new file mode 100644 index 00000000..d53067c6 --- /dev/null +++ b/client/src/main/java/org/apache/commons/net/telnet/TelnetClient.java @@ -0,0 +1,414 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.net.telnet; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/*** + * The TelnetClient class implements the simple network virtual + * terminal (NVT) for the Telnet protocol according to RFC 854. It + * does not implement any of the extra Telnet options because it + * is meant to be used within a Java program providing automated + * access to Telnet accessible resources. + *

+ * The class can be used by first connecting to a server using the + * SocketClient + * {@link org.apache.commons.net.SocketClient#connect connect} + * method. Then an InputStream and OutputStream for sending and + * receiving data over the Telnet connection can be obtained by + * using the {@link #getInputStream getInputStream() } and + * {@link #getOutputStream getOutputStream() } methods. + * When you finish using the streams, you must call + * {@link #disconnect disconnect } rather than simply + * closing the streams. + ***/ + +public class TelnetClient extends Telnet +{ + private InputStream __input; + private OutputStream __output; + protected boolean readerThread = true; + private TelnetInputListener inputListener; + + /*** + * Default TelnetClient constructor, sets terminal-type {@code VT100}. + ***/ + public TelnetClient() + { + /* TERMINAL-TYPE option (start)*/ + super ("VT100"); + /* TERMINAL-TYPE option (end)*/ + __input = null; + __output = null; + } + + /** + * Construct an instance with the specified terminal type. + * + * @param termtype the terminal type to use, e.g. {@code VT100} + */ + /* TERMINAL-TYPE option (start)*/ + public TelnetClient(String termtype) + { + super (termtype); + __input = null; + __output = null; + } + /* TERMINAL-TYPE option (end)*/ + + void _flushOutputStream() throws IOException + { + _output_.flush(); + } + void _closeOutputStream() throws IOException + { + _output_.close(); + } + + /*** + * Handles special connection requirements. + * + * @exception IOException If an error occurs during connection setup. + ***/ + @Override + protected void _connectAction_() throws IOException + { + super._connectAction_(); + TelnetInputStream tmp = new TelnetInputStream(_input_, this, readerThread); + if(readerThread) + { + tmp._start(); + } + // __input CANNOT refer to the TelnetInputStream. We run into + // blocking problems when some classes use TelnetInputStream, so + // we wrap it with a BufferedInputStream which we know is safe. + // This blocking behavior requires further investigation, but right + // now it looks like classes like InputStreamReader are not implemented + // in a safe manner. + __input = new BufferedInputStream(tmp); + __output = new TelnetOutputStream(this); + } + + /*** + * Disconnects the telnet session, closing the input and output streams + * as well as the socket. If you have references to the + * input and output streams of the telnet connection, you should not + * close them yourself, but rather call disconnect to properly close + * the connection. + ***/ + @Override + public void disconnect() throws IOException + { + if (__input != null) { + __input.close(); + } + if (__output != null) { + __output.close(); + } + super.disconnect(); + } + + /*** + * Returns the telnet connection output stream. You should not close the + * stream when you finish with it. Rather, you should call + * {@link #disconnect disconnect }. + * + * @return The telnet connection output stream. + ***/ + public OutputStream getOutputStream() + { + return __output; + } + + /*** + * Returns the telnet connection input stream. You should not close the + * stream when you finish with it. Rather, you should call + * {@link #disconnect disconnect }. + * + * @return The telnet connection input stream. + ***/ + public InputStream getInputStream() + { + return __input; + } + + /*** + * Returns the state of the option on the local side. + * + * @param option - Option to be checked. + * + * @return The state of the option on the local side. + ***/ + public boolean getLocalOptionState(int option) + { + /* BUG (option active when not already acknowledged) (start)*/ + return (_stateIsWill(option) && _requestedWill(option)); + /* BUG (option active when not already acknowledged) (end)*/ + } + + /*** + * Returns the state of the option on the remote side. + * + * @param option - Option to be checked. + * + * @return The state of the option on the remote side. + ***/ + public boolean getRemoteOptionState(int option) + { + /* BUG (option active when not already acknowledged) (start)*/ + return (_stateIsDo(option) && _requestedDo(option)); + /* BUG (option active when not already acknowledged) (end)*/ + } + /* open TelnetOptionHandler functionality (end)*/ + + /* Code Section added for supporting AYT (start)*/ + + /*** + * Sends an Are You There sequence and waits for the result. + * + * @param timeout - Time to wait for a response (millis.) + * + * @return true if AYT received a response, false otherwise + * + * @throws InterruptedException on error + * @throws IllegalArgumentException on error + * @throws IOException on error + ***/ + public boolean sendAYT(long timeout) + throws IOException, IllegalArgumentException, InterruptedException + { + return (_sendAYT(timeout)); + } + /* Code Section added for supporting AYT (start)*/ + + /*** + * Sends a protocol-specific subnegotiation message to the remote peer. + * {@link TelnetClient} will add the IAC SB & IAC SE framing bytes; + * the first byte in {@code message} should be the appropriate telnet + * option code. + * + *

+ * This method does not wait for any response. Subnegotiation messages + * sent by the remote end can be handled by registering an approrpriate + * {@link TelnetOptionHandler}. + *

+ * + * @param message option code followed by subnegotiation payload + * @throws IllegalArgumentException if {@code message} has length zero + * @throws IOException if an I/O error occurs while writing the message + * @since 3.0 + ***/ + public void sendSubnegotiation(int[] message) + throws IOException, IllegalArgumentException + { + if (message.length < 1) { + throw new IllegalArgumentException("zero length message"); + } + _sendSubnegotiation(message); + } + + /*** + * Sends a command byte to the remote peer, adding the IAC prefix. + * + *

+ * This method does not wait for any response. Messages + * sent by the remote end can be handled by registering an approrpriate + * {@link TelnetOptionHandler}. + *

+ * + * @param command the code for the command + * @throws IOException if an I/O error occurs while writing the message + * @throws IllegalArgumentException on error + * @since 3.0 + ***/ + public void sendCommand(byte command) + throws IOException, IllegalArgumentException + { + _sendCommand(command); + } + + /* open TelnetOptionHandler functionality (start)*/ + + /*** + * Registers a new TelnetOptionHandler for this telnet client to use. + * + * @param opthand - option handler to be registered. + * + * @throws InvalidTelnetOptionException on error + * @throws IOException on error + ***/ + @Override + public void addOptionHandler(TelnetOptionHandler opthand) + throws InvalidTelnetOptionException, IOException + { + super.addOptionHandler(opthand); + } + /* open TelnetOptionHandler functionality (end)*/ + + /*** + * Unregisters a TelnetOptionHandler. + * + * @param optcode - Code of the option to be unregistered. + * + * @throws InvalidTelnetOptionException on error + * @throws IOException on error + ***/ + @Override + public void deleteOptionHandler(int optcode) + throws InvalidTelnetOptionException, IOException + { + super.deleteOptionHandler(optcode); + } + + /* Code Section added for supporting spystreams (start)*/ + /*** + * Registers an OutputStream for spying what's going on in + * the TelnetClient session. + * + * @param spystream - OutputStream on which session activity + * will be echoed. + ***/ + public void registerSpyStream(OutputStream spystream) + { + super._registerSpyStream(spystream); + } + + /*** + * Stops spying this TelnetClient. + * + ***/ + public void stopSpyStream() + { + super._stopSpyStream(); + } + /* Code Section added for supporting spystreams (end)*/ + + /*** + * Registers a notification handler to which will be sent + * notifications of received telnet option negotiation commands. + * + * @param notifhand - TelnetNotificationHandler to be registered + ***/ + @Override + public void registerNotifHandler(TelnetNotificationHandler notifhand) + { + super.registerNotifHandler(notifhand); + } + + /*** + * Unregisters the current notification handler. + * + ***/ + @Override + public void unregisterNotifHandler() + { + super.unregisterNotifHandler(); + } + + /*** + * Sets the status of the reader thread. + * + *

+ * When enabled, a seaparate internal reader thread is created for new + * connections to read incoming data as it arrives. This results in + * immediate handling of option negotiation, notifications, etc. + * (at least until the fixed-size internal buffer fills up). + * Otherwise, no thread is created an all negotiation and option + * handling is deferred until a read() is performed on the + * {@link #getInputStream input stream}. + *

+ * + *

+ * The reader thread must be enabled for {@link TelnetInputListener} + * support. + *

+ * + *

+ * When this method is invoked, the reader thread status will apply to all + * subsequent connections; the current connection (if any) is not affected. + *

+ * + * @param flag true to enable the reader thread, false to disable + * @see #registerInputListener + ***/ + public void setReaderThread(boolean flag) + { + readerThread = flag; + } + + /*** + * Gets the status of the reader thread. + * + * @return true if the reader thread is enabled, false otherwise + ***/ + public boolean getReaderThread() + { + return (readerThread); + } + + /*** + * Register a listener to be notified when new incoming data is + * available to be read on the {@link #getInputStream input stream}. + * Only one listener is supported at a time. + * + *

+ * More precisely, notifications are issued whenever the number of + * bytes available for immediate reading (i.e., the value returned + * by {@link InputStream#available}) transitions from zero to non-zero. + * Note that (in general) multiple reads may be required to empty the + * buffer and reset this notification, because incoming bytes are being + * added to the internal buffer asynchronously. + *

+ * + *

+ * Notifications are only supported when a {@link #setReaderThread + * reader thread} is enabled for the connection. + *

+ * + * @param listener listener to be registered; replaces any previous + * @since 3.0 + ***/ + public synchronized void registerInputListener(TelnetInputListener listener) + { + this.inputListener = listener; + } + + /*** + * Unregisters the current {@link TelnetInputListener}, if any. + * + * @since 3.0 + ***/ + public synchronized void unregisterInputListener() + { + this.inputListener = null; + } + + // Notify input listener + void notifyInputListener() { + TelnetInputListener listener; + synchronized (this) { + listener = this.inputListener; + } + if (listener != null) { + listener.telnetInputAvailable(); + } + } +} diff --git a/client/src/main/java/org/apache/commons/net/telnet/TelnetCommand.java b/client/src/main/java/org/apache/commons/net/telnet/TelnetCommand.java new file mode 100644 index 00000000..6e65f302 --- /dev/null +++ b/client/src/main/java/org/apache/commons/net/telnet/TelnetCommand.java @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.net.telnet; + +/** + * The TelnetCommand class cannot be instantiated and only serves as a + * storehouse for telnet command constants. + * @see org.apache.commons.net.telnet.Telnet + * @see org.apache.commons.net.telnet.TelnetClient + */ + +public final class TelnetCommand +{ + /*** The maximum value a command code can have. This value is 255. ***/ + public static final int MAX_COMMAND_VALUE = 255; + + /*** Interpret As Command code. Value is 255 according to RFC 854. ***/ + public static final int IAC = 255; + + /*** Don't use option code. Value is 254 according to RFC 854. ***/ + public static final int DONT = 254; + + /*** Request to use option code. Value is 253 according to RFC 854. ***/ + public static final int DO = 253; + + /*** Refuse to use option code. Value is 252 according to RFC 854. ***/ + public static final int WONT = 252; + + /*** Agree to use option code. Value is 251 according to RFC 854. ***/ + public static final int WILL = 251; + + /*** Start subnegotiation code. Value is 250 according to RFC 854. ***/ + public static final int SB = 250; + + /*** Go Ahead code. Value is 249 according to RFC 854. ***/ + public static final int GA = 249; + + /*** Erase Line code. Value is 248 according to RFC 854. ***/ + public static final int EL = 248; + + /*** Erase Character code. Value is 247 according to RFC 854. ***/ + public static final int EC = 247; + + /*** Are You There code. Value is 246 according to RFC 854. ***/ + public static final int AYT = 246; + + /*** Abort Output code. Value is 245 according to RFC 854. ***/ + public static final int AO = 245; + + /*** Interrupt Process code. Value is 244 according to RFC 854. ***/ + public static final int IP = 244; + + /*** Break code. Value is 243 according to RFC 854. ***/ + public static final int BREAK = 243; + + /*** Data mark code. Value is 242 according to RFC 854. ***/ + public static final int DM = 242; + + /*** No Operation code. Value is 241 according to RFC 854. ***/ + public static final int NOP = 241; + + /*** End subnegotiation code. Value is 240 according to RFC 854. ***/ + public static final int SE = 240; + + /*** End of record code. Value is 239. ***/ + public static final int EOR = 239; + + /*** Abort code. Value is 238. ***/ + public static final int ABORT = 238; + + /*** Suspend process code. Value is 237. ***/ + public static final int SUSP = 237; + + /*** End of file code. Value is 236. ***/ + public static final int EOF = 236; + + /*** Synchronize code. Value is 242. ***/ + public static final int SYNCH = 242; + + /*** String representations of commands. ***/ + private static final String __commandString[] = { + "IAC", "DONT", "DO", "WONT", "WILL", "SB", "GA", "EL", "EC", "AYT", + "AO", "IP", "BRK", "DMARK", "NOP", "SE", "EOR", "ABORT", "SUSP", "EOF" + }; + + private static final int __FIRST_COMMAND = IAC; + private static final int __LAST_COMMAND = EOF; + + /*** + * Returns the string representation of the telnet protocol command + * corresponding to the given command code. + *

+ * @param code The command code of the telnet protocol command. + * @return The string representation of the telnet protocol command. + ***/ + public static final String getCommand(int code) + { + return __commandString[__FIRST_COMMAND - code]; + } + + /*** + * Determines if a given command code is valid. Returns true if valid, + * false if not. + *

+ * @param code The command code to test. + * @return True if the command code is valid, false if not. + **/ + public static final boolean isValidCommand(int code) + { + return (code <= __FIRST_COMMAND && code >= __LAST_COMMAND); + } + + // Cannot be instantiated + private TelnetCommand() + { } +} diff --git a/client/src/main/java/org/apache/commons/net/telnet/TelnetInputListener.java b/client/src/main/java/org/apache/commons/net/telnet/TelnetInputListener.java new file mode 100644 index 00000000..34bb2b6e --- /dev/null +++ b/client/src/main/java/org/apache/commons/net/telnet/TelnetInputListener.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.net.telnet; + +/*** + * Listener interface used for notification that incoming data is + * available to be read. + * + * @see TelnetClient + * @since 3.0 + ***/ +public interface TelnetInputListener +{ + + /*** + * Callback method invoked when new incoming data is available on a + * {@link TelnetClient}'s {@link TelnetClient#getInputStream input stream}. + * + * @see TelnetClient#registerInputListener + ***/ + public void telnetInputAvailable(); +} diff --git a/client/src/main/java/org/apache/commons/net/telnet/TelnetInputStream.java b/client/src/main/java/org/apache/commons/net/telnet/TelnetInputStream.java new file mode 100644 index 00000000..082bbea1 --- /dev/null +++ b/client/src/main/java/org/apache/commons/net/telnet/TelnetInputStream.java @@ -0,0 +1,680 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.net.telnet; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; + +final class TelnetInputStream extends BufferedInputStream implements Runnable +{ + /** End of file has been reached */ + private static final int EOF = -1; + + /** Read would block */ + private static final int WOULD_BLOCK = -2; + + // TODO should these be private enums? + static final int _STATE_DATA = 0, _STATE_IAC = 1, _STATE_WILL = 2, + _STATE_WONT = 3, _STATE_DO = 4, _STATE_DONT = 5, + _STATE_SB = 6, _STATE_SE = 7, _STATE_CR = 8, _STATE_IAC_SB = 9; + + private boolean __hasReachedEOF; // @GuardedBy("__queue") + private volatile boolean __isClosed; + private boolean __readIsWaiting; + private int __receiveState, __queueHead, __queueTail, __bytesAvailable; + private final int[] __queue; + private final TelnetClient __client; + private final Thread __thread; + private IOException __ioException; + + /* TERMINAL-TYPE option (start)*/ + private final int __suboption[] = new int[512]; + private int __suboption_count = 0; + /* TERMINAL-TYPE option (end)*/ + + private volatile boolean __threaded; + + TelnetInputStream(InputStream input, TelnetClient client, + boolean readerThread) + { + super(input); + __client = client; + __receiveState = _STATE_DATA; + __isClosed = true; + __hasReachedEOF = false; + // Make it 2049, because when full, one slot will go unused, and we + // want a 2048 byte buffer just to have a round number (base 2 that is) + __queue = new int[2049]; + __queueHead = 0; + __queueTail = 0; + __bytesAvailable = 0; + __ioException = null; + __readIsWaiting = false; + __threaded = false; + if(readerThread) { + __thread = new Thread(this); + } else { + __thread = null; + } + } + + TelnetInputStream(InputStream input, TelnetClient client) { + this(input, client, true); + } + + void _start() + { + if(__thread == null) { + return; + } + + int priority; + __isClosed = false; + // TODO remove this + // Need to set a higher priority in case JVM does not use pre-emptive + // threads. This should prevent scheduler induced deadlock (rather than + // deadlock caused by a bug in this code). + priority = Thread.currentThread().getPriority() + 1; + if (priority > Thread.MAX_PRIORITY) { + priority = Thread.MAX_PRIORITY; + } + __thread.setPriority(priority); + __thread.setDaemon(true); + __thread.start(); + __threaded = true; // tell _processChar that we are running threaded + } + + + // synchronized(__client) critical sections are to protect against + // TelnetOutputStream writing through the telnet client at same time + // as a processDo/Will/etc. command invoked from TelnetInputStream + // tries to write. + /** + * Get the next byte of data. + * IAC commands are processed internally and do not return data. + * + * @param mayBlock true if method is allowed to block + * @return the next byte of data, + * or -1 (EOF) if end of stread reached, + * or -2 (WOULD_BLOCK) if mayBlock is false and there is no data available + */ + private int __read(boolean mayBlock) throws IOException + { + int ch; + + while (true) + { + + // If there is no more data AND we were told not to block, + // just return WOULD_BLOCK (-2). (More efficient than exception.) + if(!mayBlock && super.available() == 0) { + return WOULD_BLOCK; + } + + // Otherwise, exit only when we reach end of stream. + if ((ch = super.read()) < 0) { + return EOF; + } + + ch = (ch & 0xff); + + /* Code Section added for supporting AYT (start)*/ + synchronized (__client) + { + __client._processAYTResponse(); + } + /* Code Section added for supporting AYT (end)*/ + + /* Code Section added for supporting spystreams (start)*/ + __client._spyRead(ch); + /* Code Section added for supporting spystreams (end)*/ + + switch (__receiveState) + { + + case _STATE_CR: + if (ch == '\0') + { + // Strip null + continue; + } + // How do we handle newline after cr? + // else if (ch == '\n' && _requestedDont(TelnetOption.ECHO) && + + // Handle as normal data by falling through to _STATE_DATA case + + //$FALL-THROUGH$ + case _STATE_DATA: + if (ch == TelnetCommand.IAC) + { + __receiveState = _STATE_IAC; + continue; + } + + + if (ch == '\r') + { + synchronized (__client) + { + if (__client._requestedDont(TelnetOption.BINARY)) { + __receiveState = _STATE_CR; + } else { + __receiveState = _STATE_DATA; + } + } + } else { + __receiveState = _STATE_DATA; + } + break; + + case _STATE_IAC: + switch (ch) + { + case TelnetCommand.WILL: + __receiveState = _STATE_WILL; + continue; + case TelnetCommand.WONT: + __receiveState = _STATE_WONT; + continue; + case TelnetCommand.DO: + __receiveState = _STATE_DO; + continue; + case TelnetCommand.DONT: + __receiveState = _STATE_DONT; + continue; + /* TERMINAL-TYPE option (start)*/ + case TelnetCommand.SB: + __suboption_count = 0; + __receiveState = _STATE_SB; + continue; + /* TERMINAL-TYPE option (end)*/ + case TelnetCommand.IAC: + __receiveState = _STATE_DATA; + break; // exit to enclosing switch to return IAC from read + case TelnetCommand.SE: // unexpected byte! ignore it (don't send it as a command) + __receiveState = _STATE_DATA; + continue; + default: + __receiveState = _STATE_DATA; + __client._processCommand(ch); // Notify the user + continue; // move on the next char + } + break; // exit and return from read + case _STATE_WILL: + synchronized (__client) + { + __client._processWill(ch); + __client._flushOutputStream(); + } + __receiveState = _STATE_DATA; + continue; + case _STATE_WONT: + synchronized (__client) + { + __client._processWont(ch); + __client._flushOutputStream(); + } + __receiveState = _STATE_DATA; + continue; + case _STATE_DO: + synchronized (__client) + { + __client._processDo(ch); + __client._flushOutputStream(); + } + __receiveState = _STATE_DATA; + continue; + case _STATE_DONT: + synchronized (__client) + { + __client._processDont(ch); + __client._flushOutputStream(); + } + __receiveState = _STATE_DATA; + continue; + /* TERMINAL-TYPE option (start)*/ + case _STATE_SB: + switch (ch) + { + case TelnetCommand.IAC: + __receiveState = _STATE_IAC_SB; + continue; + default: + // store suboption char + if (__suboption_count < __suboption.length) { + __suboption[__suboption_count++] = ch; + } + break; + } + __receiveState = _STATE_SB; + continue; + case _STATE_IAC_SB: // IAC received during SB phase + switch (ch) + { + case TelnetCommand.SE: + synchronized (__client) + { + __client._processSuboption(__suboption, __suboption_count); + __client._flushOutputStream(); + } + __receiveState = _STATE_DATA; + continue; + case TelnetCommand.IAC: // De-dup the duplicated IAC + if (__suboption_count < __suboption.length) { + __suboption[__suboption_count++] = ch; + } + break; + default: // unexpected byte! ignore it + break; + } + __receiveState = _STATE_SB; + continue; + /* TERMINAL-TYPE option (end)*/ + } + + break; + } + + return ch; + } + + // synchronized(__client) critical sections are to protect against + // TelnetOutputStream writing through the telnet client at same time + // as a processDo/Will/etc. command invoked from TelnetInputStream + // tries to write. Returns true if buffer was previously empty. + private boolean __processChar(int ch) throws InterruptedException + { + // Critical section because we're altering __bytesAvailable, + // __queueTail, and the contents of _queue. + boolean bufferWasEmpty; + synchronized (__queue) + { + bufferWasEmpty = (__bytesAvailable == 0); + while (__bytesAvailable >= __queue.length - 1) + { + // The queue is full. We need to wait before adding any more data to it. Hopefully the stream owner + // will consume some data soon! + if(__threaded) + { + __queue.notify(); + try + { + __queue.wait(); + } + catch (InterruptedException e) + { + throw e; + } + } + else + { + // We've been asked to add another character to the queue, but it is already full and there's + // no other thread to drain it. This should not have happened! + throw new IllegalStateException("Queue is full! Cannot process another character."); + } + } + + // Need to do this in case we're not full, but block on a read + if (__readIsWaiting && __threaded) + { + __queue.notify(); + } + + __queue[__queueTail] = ch; + ++__bytesAvailable; + + if (++__queueTail >= __queue.length) { + __queueTail = 0; + } + } + return bufferWasEmpty; + } + + @Override + public int read() throws IOException + { + // Critical section because we're altering __bytesAvailable, + // __queueHead, and the contents of _queue in addition to + // testing value of __hasReachedEOF. + synchronized (__queue) + { + + while (true) + { + if (__ioException != null) + { + IOException e; + e = __ioException; + __ioException = null; + throw e; + } + + if (__bytesAvailable == 0) + { + // Return EOF if at end of file + if (__hasReachedEOF) { + return EOF; + } + + // Otherwise, we have to wait for queue to get something + if(__threaded) + { + __queue.notify(); + try + { + __readIsWaiting = true; + __queue.wait(); + __readIsWaiting = false; + } + catch (InterruptedException e) + { + throw new InterruptedIOException("Fatal thread interruption during read."); + } + } + else + { + //__alreadyread = false; + __readIsWaiting = true; + int ch; + boolean mayBlock = true; // block on the first read only + + do + { + try + { + if ((ch = __read(mayBlock)) < 0) { // must be EOF + if(ch != WOULD_BLOCK) { + return (ch); + } + } + } + catch (InterruptedIOException e) + { + synchronized (__queue) + { + __ioException = e; + __queue.notifyAll(); + try + { + __queue.wait(100); + } + catch (InterruptedException interrupted) + { + // Ignored + } + } + return EOF; + } + + + try + { + if(ch != WOULD_BLOCK) + { + __processChar(ch); + } + } + catch (InterruptedException e) + { + if (__isClosed) { + return EOF; + } + } + + // Reads should not block on subsequent iterations. Potentially, this could happen if the + // remaining buffered socket data consists entirely of Telnet command sequence and no "user" data. + mayBlock = false; + + } + // Continue reading as long as there is data available and the queue is not full. + while (super.available() > 0 && __bytesAvailable < __queue.length - 1); + + __readIsWaiting = false; + } + continue; + } + else + { + int ch; + + ch = __queue[__queueHead]; + + if (++__queueHead >= __queue.length) { + __queueHead = 0; + } + + --__bytesAvailable; + + // Need to explicitly notify() so available() works properly + if(__bytesAvailable == 0 && __threaded) { + __queue.notify(); + } + + return ch; + } + } + } + } + + + /*** + * Reads the next number of bytes from the stream into an array and + * returns the number of bytes read. Returns -1 if the end of the + * stream has been reached. + *

+ * @param buffer The byte array in which to store the data. + * @return The number of bytes read. Returns -1 if the + * end of the message has been reached. + * @exception IOException If an error occurs in reading the underlying + * stream. + ***/ + @Override + public int read(byte buffer[]) throws IOException + { + return read(buffer, 0, buffer.length); + } + + + /*** + * Reads the next number of bytes from the stream into an array and returns + * the number of bytes read. Returns -1 if the end of the + * message has been reached. The characters are stored in the array + * starting from the given offset and up to the length specified. + *

+ * @param buffer The byte array in which to store the data. + * @param offset The offset into the array at which to start storing data. + * @param length The number of bytes to read. + * @return The number of bytes read. Returns -1 if the + * end of the stream has been reached. + * @exception IOException If an error occurs while reading the underlying + * stream. + ***/ + @Override + public int read(byte buffer[], int offset, int length) throws IOException + { + int ch, off; + + if (length < 1) { + return 0; + } + + // Critical section because run() may change __bytesAvailable + synchronized (__queue) + { + if (length > __bytesAvailable) { + length = __bytesAvailable; + } + } + + if ((ch = read()) == EOF) { + return EOF; + } + + off = offset; + + do + { + buffer[offset++] = (byte)ch; + } + while (--length > 0 && (ch = read()) != EOF); + + //__client._spyRead(buffer, off, offset - off); + return (offset - off); + } + + + /*** Returns false. Mark is not supported. ***/ + @Override + public boolean markSupported() + { + return false; + } + + @Override + public int available() throws IOException + { + // Critical section because run() may change __bytesAvailable + synchronized (__queue) + { + if (__threaded) { // Must not call super.available when running threaded: NET-466 + return __bytesAvailable; + } else { + return __bytesAvailable + super.available(); + } + } + } + + + // Cannot be synchronized. Will cause deadlock if run() is blocked + // in read because BufferedInputStream read() is synchronized. + @Override + public void close() throws IOException + { + // Completely disregard the fact thread may still be running. + // We can't afford to block on this close by waiting for + // thread to terminate because few if any JVM's will actually + // interrupt a system read() from the interrupt() method. + super.close(); + + synchronized (__queue) + { + __hasReachedEOF = true; + __isClosed = true; + + if (__thread != null && __thread.isAlive()) + { + __thread.interrupt(); + } + + __queue.notifyAll(); + } + + } + + @Override + public void run() + { + int ch; + + try + { +_outerLoop: + while (!__isClosed) + { + try + { + if ((ch = __read(true)) < 0) { + break; + } + } + catch (InterruptedIOException e) + { + synchronized (__queue) + { + __ioException = e; + __queue.notifyAll(); + try + { + __queue.wait(100); + } + catch (InterruptedException interrupted) + { + if (__isClosed) { + break _outerLoop; + } + } + continue; + } + } catch(RuntimeException re) { + // We treat any runtime exceptions as though the + // stream has been closed. We close the + // underlying stream just to be sure. + super.close(); + // Breaking the loop has the effect of setting + // the state to closed at the end of the method. + break _outerLoop; + } + + // Process new character + boolean notify = false; + try + { + notify = __processChar(ch); + } + catch (InterruptedException e) + { + if (__isClosed) { + break _outerLoop; + } + } + + // Notify input listener if buffer was previously empty + if (notify) { + __client.notifyInputListener(); + } + } + } + catch (IOException ioe) + { + synchronized (__queue) + { + __ioException = ioe; + } + __client.notifyInputListener(); + } + + synchronized (__queue) + { + __isClosed = true; // Possibly redundant + __hasReachedEOF = true; + __queue.notify(); + } + + __threaded = false; + } +} + +/* Emacs configuration + * Local variables: ** + * mode: java ** + * c-basic-offset: 4 ** + * indent-tabs-mode: nil ** + * End: ** + */ diff --git a/client/src/main/java/org/apache/commons/net/telnet/TelnetNotificationHandler.java b/client/src/main/java/org/apache/commons/net/telnet/TelnetNotificationHandler.java new file mode 100644 index 00000000..19446dd1 --- /dev/null +++ b/client/src/main/java/org/apache/commons/net/telnet/TelnetNotificationHandler.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.net.telnet; + +/*** + * The TelnetNotificationHandler interface can be used to handle + * notification of options negotiation commands received on a telnet + * session. + *

+ * The user can implement this interface and register a + * TelnetNotificationHandler by using the registerNotificationHandler() + * of TelnetClient to be notified of option negotiation commands. + ***/ + +public interface TelnetNotificationHandler +{ + /*** + * The remote party sent a DO command. + ***/ + public static final int RECEIVED_DO = 1; + + /*** + * The remote party sent a DONT command. + ***/ + public static final int RECEIVED_DONT = 2; + + /*** + * The remote party sent a WILL command. + ***/ + public static final int RECEIVED_WILL = 3; + + /*** + * The remote party sent a WONT command. + ***/ + public static final int RECEIVED_WONT = 4; + + /*** + * The remote party sent a COMMAND. + * @since 2.2 + ***/ + public static final int RECEIVED_COMMAND = 5; + + /*** + * Callback method called when TelnetClient receives an + * command or option negotiation command + * + * @param negotiation_code - type of (negotiation) command received + * (RECEIVED_DO, RECEIVED_DONT, RECEIVED_WILL, RECEIVED_WONT, RECEIVED_COMMAND) + * + * @param option_code - code of the option negotiated, or the command code itself (e.g. NOP). + ***/ + public void receivedNegotiation(int negotiation_code, int option_code); +} diff --git a/client/src/main/java/org/apache/commons/net/telnet/TelnetOption.java b/client/src/main/java/org/apache/commons/net/telnet/TelnetOption.java new file mode 100644 index 00000000..5fa7d5d0 --- /dev/null +++ b/client/src/main/java/org/apache/commons/net/telnet/TelnetOption.java @@ -0,0 +1,193 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.net.telnet; + +/*** + * The TelnetOption class cannot be instantiated and only serves as a + * storehouse for telnet option constants. + *

+ * Details regarding Telnet option specification can be found in RFC 855. + * + * + * @see org.apache.commons.net.telnet.Telnet + * @see org.apache.commons.net.telnet.TelnetClient + ***/ + +public class TelnetOption +{ + /*** The maximum value an option code can have. This value is 255. ***/ + public static final int MAX_OPTION_VALUE = 255; + + public static final int BINARY = 0; + + public static final int ECHO = 1; + + public static final int PREPARE_TO_RECONNECT = 2; + + public static final int SUPPRESS_GO_AHEAD = 3; + + public static final int APPROXIMATE_MESSAGE_SIZE = 4; + + public static final int STATUS = 5; + + public static final int TIMING_MARK = 6; + + public static final int REMOTE_CONTROLLED_TRANSMISSION = 7; + + public static final int NEGOTIATE_OUTPUT_LINE_WIDTH = 8; + + public static final int NEGOTIATE_OUTPUT_PAGE_SIZE = 9; + + public static final int NEGOTIATE_CARRIAGE_RETURN = 10; + + public static final int NEGOTIATE_HORIZONTAL_TAB_STOP = 11; + + public static final int NEGOTIATE_HORIZONTAL_TAB = 12; + + public static final int NEGOTIATE_FORMFEED = 13; + + public static final int NEGOTIATE_VERTICAL_TAB_STOP = 14; + + public static final int NEGOTIATE_VERTICAL_TAB = 15; + + public static final int NEGOTIATE_LINEFEED = 16; + + public static final int EXTENDED_ASCII = 17; + + public static final int FORCE_LOGOUT = 18; + + public static final int BYTE_MACRO = 19; + + public static final int DATA_ENTRY_TERMINAL = 20; + + public static final int SUPDUP = 21; + + public static final int SUPDUP_OUTPUT = 22; + + public static final int SEND_LOCATION = 23; + + public static final int TERMINAL_TYPE = 24; + + public static final int END_OF_RECORD = 25; + + public static final int TACACS_USER_IDENTIFICATION = 26; + + public static final int OUTPUT_MARKING = 27; + + public static final int TERMINAL_LOCATION_NUMBER = 28; + + public static final int REGIME_3270 = 29; + + public static final int X3_PAD = 30; + + public static final int WINDOW_SIZE = 31; + + public static final int TERMINAL_SPEED = 32; + + public static final int REMOTE_FLOW_CONTROL = 33; + + public static final int LINEMODE = 34; + + public static final int X_DISPLAY_LOCATION = 35; + + public static final int OLD_ENVIRONMENT_VARIABLES = 36; + + public static final int AUTHENTICATION = 37; + + public static final int ENCRYPTION = 38; + + public static final int NEW_ENVIRONMENT_VARIABLES = 39; + + public static final int EXTENDED_OPTIONS_LIST = 255; + + @SuppressWarnings("unused") + private static final int __FIRST_OPTION = BINARY; + private static final int __LAST_OPTION = EXTENDED_OPTIONS_LIST; + + private static final String __optionString[] = { + "BINARY", "ECHO", "RCP", "SUPPRESS GO AHEAD", "NAME", "STATUS", + "TIMING MARK", "RCTE", "NAOL", "NAOP", "NAOCRD", "NAOHTS", "NAOHTD", + "NAOFFD", "NAOVTS", "NAOVTD", "NAOLFD", "EXTEND ASCII", "LOGOUT", + "BYTE MACRO", "DATA ENTRY TERMINAL", "SUPDUP", "SUPDUP OUTPUT", + "SEND LOCATION", "TERMINAL TYPE", "END OF RECORD", "TACACS UID", + "OUTPUT MARKING", "TTYLOC", "3270 REGIME", "X.3 PAD", "NAWS", "TSPEED", + "LFLOW", "LINEMODE", "XDISPLOC", "OLD-ENVIRON", "AUTHENTICATION", + "ENCRYPT", "NEW-ENVIRON", "TN3270E", "XAUTH", "CHARSET", "RSP", + "Com Port Control", "Suppress Local Echo", "Start TLS", + "KERMIT", "SEND-URL", "FORWARD_X", "", "", "", + "", "", "", "", "", "", "", "", "", "", + "", "", "", "", "", "", "", "", "", "", + "", "", "", "", "", "", "", "", "", "", + "", "", "", "", "", "", "", "", "", "", + "", "", "", "", "", "", "", "", "", "", + "", "", "", "", "", "", "", "", "", "", + "", "", "", "", "", "", "", "", "", "", + "", "", "", "", "", "", "", "", "", "", + "", "", "", "", "", "TELOPT PRAGMA LOGON", "TELOPT SSPI LOGON", + "TELOPT PRAGMA HEARTBEAT", "", "", "", "", + "", "", "", "", "", "", "", "", "", "", + "", "", "", "", "", "", "", "", "", "", + "", "", "", "", "", "", "", "", "", "", + "", "", "", "", "", "", "", "", "", "", + "", "", "", "", "", "", "", "", "", "", + "", "", "", "", "", "", "", "", "", "", + "", "", "", "", "", "", "", "", "", "", + "", "", "", "", "", "", "", "", "", "", + "", "", "", "", "", "", "", "", "", "", + "", "", "", "", "", "", "", "", "", "", + "", "", "", "", "", "", "", "", "", "", + "Extended-Options-List" + }; + + + /*** + * Returns the string representation of the telnet protocol option + * corresponding to the given option code. + * + * @param code The option code of the telnet protocol option + * @return The string representation of the telnet protocol option. + ***/ + public static final String getOption(int code) + { + if(__optionString[code].length() == 0) + { + return "UNASSIGNED"; + } + else + { + return __optionString[code]; + } + } + + + /*** + * Determines if a given option code is valid. Returns true if valid, + * false if not. + * + * @param code The option code to test. + * @return True if the option code is valid, false if not. + **/ + public static final boolean isValidOption(int code) + { + return (code <= __LAST_OPTION); + } + + // Cannot be instantiated + private TelnetOption() + { } +} diff --git a/client/src/main/java/org/apache/commons/net/telnet/TelnetOptionHandler.java b/client/src/main/java/org/apache/commons/net/telnet/TelnetOptionHandler.java new file mode 100644 index 00000000..14b4f015 --- /dev/null +++ b/client/src/main/java/org/apache/commons/net/telnet/TelnetOptionHandler.java @@ -0,0 +1,283 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.net.telnet; + +/*** + * The TelnetOptionHandler class is the base class to be used + * for implementing handlers for telnet options. + *

+ * TelnetOptionHandler implements basic option handling + * functionality and defines abstract methods that must be + * implemented to define subnegotiation behaviour. + ***/ +public abstract class TelnetOptionHandler +{ + /*** + * Option code + ***/ + private int optionCode = -1; + + /*** + * true if the option should be activated on the local side + ***/ + private boolean initialLocal = false; + + /*** + * true if the option should be activated on the remote side + ***/ + private boolean initialRemote = false; + + /*** + * true if the option should be accepted on the local side + ***/ + private boolean acceptLocal = false; + + /*** + * true if the option should be accepted on the remote side + ***/ + private boolean acceptRemote = false; + + /*** + * true if the option is active on the local side + ***/ + private boolean doFlag = false; + + /*** + * true if the option is active on the remote side + ***/ + private boolean willFlag = false; + + /*** + * Constructor for the TelnetOptionHandler. Allows defining desired + * initial setting for local/remote activation of this option and + * behaviour in case a local/remote activation request for this + * option is received. + *

+ * @param optcode - Option code. + * @param initlocal - if set to true, a WILL is sent upon connection. + * @param initremote - if set to true, a DO is sent upon connection. + * @param acceptlocal - if set to true, any DO request is accepted. + * @param acceptremote - if set to true, any WILL request is accepted. + ***/ + public TelnetOptionHandler(int optcode, + boolean initlocal, + boolean initremote, + boolean acceptlocal, + boolean acceptremote) + { + optionCode = optcode; + initialLocal = initlocal; + initialRemote = initremote; + acceptLocal = acceptlocal; + acceptRemote = acceptremote; + } + + + /*** + * Returns the option code for this option. + *

+ * @return Option code. + ***/ + public int getOptionCode() + { + return (optionCode); + } + + /*** + * Returns a boolean indicating whether to accept a DO + * request coming from the other end. + *

+ * @return true if a DO request shall be accepted. + ***/ + public boolean getAcceptLocal() + { + return (acceptLocal); + } + + /*** + * Returns a boolean indicating whether to accept a WILL + * request coming from the other end. + *

+ * @return true if a WILL request shall be accepted. + ***/ + public boolean getAcceptRemote() + { + return (acceptRemote); + } + + /*** + * Set behaviour of the option for DO requests coming from + * the other end. + *

+ * @param accept - if true, subsequent DO requests will be accepted. + ***/ + public void setAcceptLocal(boolean accept) + { + acceptLocal = accept; + } + + /*** + * Set behaviour of the option for WILL requests coming from + * the other end. + *

+ * @param accept - if true, subsequent WILL requests will be accepted. + ***/ + public void setAcceptRemote(boolean accept) + { + acceptRemote = accept; + } + + /*** + * Returns a boolean indicating whether to send a WILL request + * to the other end upon connection. + *

+ * @return true if a WILL request shall be sent upon connection. + ***/ + public boolean getInitLocal() + { + return (initialLocal); + } + + /*** + * Returns a boolean indicating whether to send a DO request + * to the other end upon connection. + *

+ * @return true if a DO request shall be sent upon connection. + ***/ + public boolean getInitRemote() + { + return (initialRemote); + } + + /*** + * Tells this option whether to send a WILL request upon connection. + *

+ * @param init - if true, a WILL request will be sent upon subsequent + * connections. + ***/ + public void setInitLocal(boolean init) + { + initialLocal = init; + } + + /*** + * Tells this option whether to send a DO request upon connection. + *

+ * @param init - if true, a DO request will be sent upon subsequent + * connections. + ***/ + public void setInitRemote(boolean init) + { + initialRemote = init; + } + + /*** + * Method called upon reception of a subnegotiation for this option + * coming from the other end. + *

+ * This implementation returns null, and + * must be overridden by the actual TelnetOptionHandler to specify + * which response must be sent for the subnegotiation request. + *

+ * @param suboptionData - the sequence received, without IAC SB & IAC SE + * @param suboptionLength - the length of data in suboption_data + *

+ * @return response to be sent to the subnegotiation sequence. TelnetClient + * will add IAC SB & IAC SE. null means no response + ***/ + public int[] answerSubnegotiation(int suboptionData[], int suboptionLength) { + return null; + } + + /*** + * This method is invoked whenever this option is acknowledged active on + * the local end (TelnetClient sent a WILL, remote side sent a DO). + * The method is used to specify a subnegotiation sequence that will be + * sent by TelnetClient when the option is activated. + *

+ * This implementation returns null, and must be overriden by + * the actual TelnetOptionHandler to specify + * which response must be sent for the subnegotiation request. + * @return subnegotiation sequence to be sent by TelnetClient. TelnetClient + * will add IAC SB & IAC SE. null means no subnegotiation. + ***/ + public int[] startSubnegotiationLocal() { + return null; + } + + /*** + * This method is invoked whenever this option is acknowledged active on + * the remote end (TelnetClient sent a DO, remote side sent a WILL). + * The method is used to specify a subnegotiation sequence that will be + * sent by TelnetClient when the option is activated. + *

+ * This implementation returns null, and must be overriden by + * the actual TelnetOptionHandler to specify + * which response must be sent for the subnegotiation request. + * @return subnegotiation sequence to be sent by TelnetClient. TelnetClient + * will add IAC SB & IAC SE. null means no subnegotiation. + ***/ + public int[] startSubnegotiationRemote() { + return null; + } + + /*** + * Returns a boolean indicating whether a WILL request sent to the other + * side has been acknowledged. + *

+ * @return true if a WILL sent to the other side has been acknowledged. + ***/ + boolean getWill() + { + return willFlag; + } + + /*** + * Tells this option whether a WILL request sent to the other + * side has been acknowledged (invoked by TelnetClient). + *

+ * @param state - if true, a WILL request has been acknowledged. + ***/ + void setWill(boolean state) + { + willFlag = state; + } + + /*** + * Returns a boolean indicating whether a DO request sent to the other + * side has been acknowledged. + *

+ * @return true if a DO sent to the other side has been acknowledged. + ***/ + boolean getDo() + { + return doFlag; + } + + + /*** + * Tells this option whether a DO request sent to the other + * side has been acknowledged (invoked by TelnetClient). + *

+ * @param state - if true, a DO request has been acknowledged. + ***/ + void setDo(boolean state) + { + doFlag = state; + } +} diff --git a/client/src/main/java/org/apache/commons/net/telnet/TelnetOutputStream.java b/client/src/main/java/org/apache/commons/net/telnet/TelnetOutputStream.java new file mode 100644 index 00000000..2f99f3ee --- /dev/null +++ b/client/src/main/java/org/apache/commons/net/telnet/TelnetOutputStream.java @@ -0,0 +1,165 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.net.telnet; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Wraps an output stream. + *

+ * In binary mode, the only conversion is to double IAC. + *

+ * In ASCII mode, if convertCRtoCRLF is true (currently always true), any CR is converted to CRLF. + * IACs are doubled. + * Also a bare LF is converted to CRLF and a bare CR is converted to CR\0 + *

+ ***/ + + +final class TelnetOutputStream extends OutputStream +{ + private final TelnetClient __client; + // TODO there does not appear to be any way to change this value - should it be a ctor parameter? + private final boolean __convertCRtoCRLF = true; + private boolean __lastWasCR = false; + + TelnetOutputStream(TelnetClient client) + { + __client = client; + } + + + /*** + * Writes a byte to the stream. + *

+ * @param ch The byte to write. + * @exception IOException If an error occurs while writing to the underlying + * stream. + ***/ + @Override + public void write(int ch) throws IOException + { + + synchronized (__client) + { + ch &= 0xff; + + if (__client._requestedWont(TelnetOption.BINARY)) // i.e. ASCII + { + if (__lastWasCR) + { + if (__convertCRtoCRLF) + { + __client._sendByte('\n'); + if (ch == '\n') // i.e. was CRLF anyway + { + __lastWasCR = false; + return ; + } + } // __convertCRtoCRLF + else if (ch != '\n') + { + __client._sendByte('\0'); // RFC854 requires CR NUL for bare CR + } + } + + switch (ch) + { + case '\r': + __client._sendByte('\r'); + __lastWasCR = true; + break; + case '\n': + if (!__lastWasCR) { // convert LF to CRLF + __client._sendByte('\r'); + } + __client._sendByte(ch); + __lastWasCR = false; + break; + case TelnetCommand.IAC: + __client._sendByte(TelnetCommand.IAC); + __client._sendByte(TelnetCommand.IAC); + __lastWasCR = false; + break; + default: + __client._sendByte(ch); + __lastWasCR = false; + break; + } + } // end ASCII + else if (ch == TelnetCommand.IAC) + { + __client._sendByte(ch); + __client._sendByte(TelnetCommand.IAC); + } else { + __client._sendByte(ch); + } + } + } + + + /*** + * Writes a byte array to the stream. + *

+ * @param buffer The byte array to write. + * @exception IOException If an error occurs while writing to the underlying + * stream. + ***/ + @Override + public void write(byte buffer[]) throws IOException + { + write(buffer, 0, buffer.length); + } + + + /*** + * Writes a number of bytes from a byte array to the stream starting from + * a given offset. + *

+ * @param buffer The byte array to write. + * @param offset The offset into the array at which to start copying data. + * @param length The number of bytes to write. + * @exception IOException If an error occurs while writing to the underlying + * stream. + ***/ + @Override + public void write(byte buffer[], int offset, int length) throws IOException + { + synchronized (__client) + { + while (length-- > 0) { + write(buffer[offset++]); + } + } + } + + /*** Flushes the stream. ***/ + @Override + public void flush() throws IOException + { + __client._flushOutputStream(); + } + + /*** Closes the stream. ***/ + @Override + public void close() throws IOException + { + __client._closeOutputStream(); + } +} diff --git a/client/src/main/java/org/apache/commons/net/telnet/TerminalTypeOptionHandler.java b/client/src/main/java/org/apache/commons/net/telnet/TerminalTypeOptionHandler.java new file mode 100644 index 00000000..49e0fa3c --- /dev/null +++ b/client/src/main/java/org/apache/commons/net/telnet/TerminalTypeOptionHandler.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.net.telnet; + +/*** + * Implements the telnet terminal type option RFC 1091. + ***/ +public class TerminalTypeOptionHandler extends TelnetOptionHandler +{ + /*** + * Terminal type + ***/ + private final String termType; + + /*** + * Terminal type option + ***/ + protected static final int TERMINAL_TYPE = 24; + + /*** + * Send (for subnegotiation) + ***/ + protected static final int TERMINAL_TYPE_SEND = 1; + + /*** + * Is (for subnegotiation) + ***/ + protected static final int TERMINAL_TYPE_IS = 0; + + /*** + * Constructor for the TerminalTypeOptionHandler. Allows defining desired + * initial setting for local/remote activation of this option and + * behaviour in case a local/remote activation request for this + * option is received. + *

+ * @param termtype - terminal type that will be negotiated. + * @param initlocal - if set to true, a WILL is sent upon connection. + * @param initremote - if set to true, a DO is sent upon connection. + * @param acceptlocal - if set to true, any DO request is accepted. + * @param acceptremote - if set to true, any WILL request is accepted. + ***/ + public TerminalTypeOptionHandler(String termtype, + boolean initlocal, + boolean initremote, + boolean acceptlocal, + boolean acceptremote) + { + super(TelnetOption.TERMINAL_TYPE, initlocal, initremote, + acceptlocal, acceptremote); + termType = termtype; + } + + /*** + * Constructor for the TerminalTypeOptionHandler. Initial and accept + * behaviour flags are set to false + *

+ * @param termtype - terminal type that will be negotiated. + ***/ + public TerminalTypeOptionHandler(String termtype) + { + super(TelnetOption.TERMINAL_TYPE, false, false, false, false); + termType = termtype; + } + + /*** + * Implements the abstract method of TelnetOptionHandler. + *

+ * @param suboptionData - the sequence received, without IAC SB & IAC SE + * @param suboptionLength - the length of data in suboption_data + *

+ * @return terminal type information + ***/ + @Override + public int[] answerSubnegotiation(int suboptionData[], int suboptionLength) + { + if ((suboptionData != null) && (suboptionLength > 1) + && (termType != null)) + { + if ((suboptionData[0] == TERMINAL_TYPE) + && (suboptionData[1] == TERMINAL_TYPE_SEND)) + { + int response[] = new int[termType.length() + 2]; + + response[0] = TERMINAL_TYPE; + response[1] = TERMINAL_TYPE_IS; + + for (int ii = 0; ii < termType.length(); ii++) + { + response[ii + 2] = termType.charAt(ii); + } + + return response; + } + } + return null; + } +} diff --git a/client/src/main/java/org/apache/commons/net/telnet/WindowSizeOptionHandler.java b/client/src/main/java/org/apache/commons/net/telnet/WindowSizeOptionHandler.java new file mode 100644 index 00000000..e1ba7695 --- /dev/null +++ b/client/src/main/java/org/apache/commons/net/telnet/WindowSizeOptionHandler.java @@ -0,0 +1,162 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.net.telnet; + +/*** + * Implements the telnet window size option RFC 1073. + * @version $Id: WindowSizeOptionHandler.java 1697293 2015-08-24 01:01:00Z sebb $ + * @since 2.0 + ***/ +public class WindowSizeOptionHandler extends TelnetOptionHandler +{ + /*** + * Horizontal Size + ***/ + private int m_nWidth = 80; + + /*** + * Vertical Size + ***/ + private int m_nHeight = 24; + + /*** + * Window size option + ***/ + protected static final int WINDOW_SIZE = 31; + + /*** + * Constructor for the WindowSizeOptionHandler. Allows defining desired + * initial setting for local/remote activation of this option and + * behaviour in case a local/remote activation request for this + * option is received. + *

+ * @param nWidth - Window width. + * @param nHeight - Window Height + * @param initlocal - if set to true, a WILL is sent upon connection. + * @param initremote - if set to true, a DO is sent upon connection. + * @param acceptlocal - if set to true, any DO request is accepted. + * @param acceptremote - if set to true, any WILL request is accepted. + ***/ + public WindowSizeOptionHandler( + int nWidth, + int nHeight, + boolean initlocal, + boolean initremote, + boolean acceptlocal, + boolean acceptremote + ) { + super ( + TelnetOption.WINDOW_SIZE, + initlocal, + initremote, + acceptlocal, + acceptremote + ); + + m_nWidth = nWidth; + m_nHeight = nHeight; + } + + /*** + * Constructor for the WindowSizeOptionHandler. Initial and accept + * behaviour flags are set to false + *

+ * @param nWidth - Window width. + * @param nHeight - Window Height + ***/ + public WindowSizeOptionHandler( + int nWidth, + int nHeight + ) { + super ( + TelnetOption.WINDOW_SIZE, + false, + false, + false, + false + ); + + m_nWidth = nWidth; + m_nHeight = nHeight; + } + + /*** + * Implements the abstract method of TelnetOptionHandler. + * This will send the client Height and Width to the server. + *

+ * @return array to send to remote system + ***/ + @Override + public int[] startSubnegotiationLocal() + { + int nCompoundWindowSize = m_nWidth * 0x10000 + m_nHeight; + int nResponseSize = 5; + int nIndex; + int nShift; + int nTurnedOnBits; + + if ((m_nWidth % 0x100) == 0xFF) { + nResponseSize += 1; + } + + if ((m_nWidth / 0x100) == 0xFF) { + nResponseSize += 1; + } + + if ((m_nHeight % 0x100) == 0xFF) { + nResponseSize += 1; + } + + if ((m_nHeight / 0x100) == 0xFF) { + nResponseSize += 1; + } + + // + // allocate response array + // + int response[] = new int[nResponseSize]; + + // + // Build response array. + // --------------------- + // 1. put option name. + // 2. loop through Window size and fill the values, + // 3. duplicate 'ff' if needed. + // + + response[0] = WINDOW_SIZE; // 1 // + + for ( // 2 // + nIndex=1, nShift = 24; + nIndex < nResponseSize; + nIndex++, nShift -=8 + ) { + nTurnedOnBits = 0xFF; + nTurnedOnBits <<= nShift; + response[nIndex] = (nCompoundWindowSize & nTurnedOnBits) >>> nShift; + + if (response[nIndex] == 0xff) { // 3 // + nIndex++; + response[nIndex] = 0xff; + } + } + + return response; + } + +} diff --git a/client/src/main/java/org/apache/commons/net/util/ListenerList.java b/client/src/main/java/org/apache/commons/net/util/ListenerList.java new file mode 100644 index 00000000..10a3ffce --- /dev/null +++ b/client/src/main/java/org/apache/commons/net/util/ListenerList.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.net.util; + +import java.io.Serializable; +import java.util.EventListener; +import java.util.Iterator; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + */ + +public class ListenerList implements Serializable, Iterable +{ + private static final long serialVersionUID = -1934227607974228213L; + + private final CopyOnWriteArrayList __listeners; + + public ListenerList() + { + __listeners = new CopyOnWriteArrayList(); + } + + public void addListener(EventListener listener) + { + __listeners.add(listener); + } + + public void removeListener(EventListener listener) + { + __listeners.remove(listener); + } + + public int getListenerCount() + { + return __listeners.size(); + } + + /** + * Return an {@link Iterator} for the {@link EventListener} instances. + * + * @return an {@link Iterator} for the {@link EventListener} instances + * @since 2.0 + * TODO Check that this is a good defensive strategy + */ + @Override + public Iterator iterator() { + return __listeners.iterator(); + } + +} diff --git a/core/pom.xml b/core/pom.xml new file mode 100644 index 00000000..4c1132de --- /dev/null +++ b/core/pom.xml @@ -0,0 +1,125 @@ + + + 4.0.0 + + com.taobao.arthas + arthas-all + 3.0.0-SNAPSHOT + + arthas-core + arthas-core + + + arthas-core + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + UTF-8 + true + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + attached + + package + + + jar-with-dependencies + + + + com.taobao.arthas.core.Arthas + + + core engine team, middleware group, alibaba inc. + + + + + + + + + + + + + org.ow2.asm + asm + + + org.ow2.asm + asm-commons + + + + + + + + + com.alibaba.middleware + termd-core + + + com.alibaba.middleware + cli + + + com.taobao.text + text.ui + + + com.fifesoft + rsyntaxtextarea + + + + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + ch.qos.logback + logback-core + + + com.taobao.middleware + logger.api + + + com.alibaba + fastjson + + + ognl + ognl + + + junit + junit + + + org.benf + cfr + + + + diff --git a/core/src/main/java/com/taobao/arthas/core/Arthas.java b/core/src/main/java/com/taobao/arthas/core/Arthas.java new file mode 100644 index 00000000..f5d4d90c --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/Arthas.java @@ -0,0 +1,93 @@ +package com.taobao.arthas.core; + +import com.taobao.arthas.core.config.Configure; +import com.taobao.middleware.cli.CLI; +import com.taobao.middleware.cli.CLIs; +import com.taobao.middleware.cli.CommandLine; +import com.taobao.middleware.cli.Option; +import com.taobao.middleware.cli.TypedOption; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; + +/** + * Arthas启动器 + */ +public class Arthas { + + private static final String DEFAULT_TELNET_PORT = "3658"; + private static final String DEFAULT_HTTP_PORT = "8563"; + + private Arthas(String[] args) throws Exception { + attachAgent(parse(args)); + } + + private Configure parse(String[] args) { + Option pid = new TypedOption().setType(Integer.class).setShortName("pid").setRequired(true); + Option core = new TypedOption().setType(String.class).setShortName("core").setRequired(true); + Option agent = new TypedOption().setType(String.class).setShortName("agent").setRequired(true); + Option target = new TypedOption().setType(String.class).setShortName("target-ip"); + Option telnetPort = new TypedOption().setType(Integer.class) + .setShortName("telnet-port").setDefaultValue(DEFAULT_TELNET_PORT); + Option httpPort = new TypedOption().setType(Integer.class) + .setShortName("http-port").setDefaultValue(DEFAULT_HTTP_PORT); + CLI cli = CLIs.create("arthas").addOption(pid).addOption(core).addOption(agent).addOption(target) + .addOption(telnetPort).addOption(httpPort); + CommandLine commandLine = cli.parse(Arrays.asList(args)); + + Configure configure = new Configure(); + configure.setJavaPid((Integer) commandLine.getOptionValue("pid")); + configure.setArthasAgent((String) commandLine.getOptionValue("agent")); + configure.setArthasCore((String) commandLine.getOptionValue("core")); + if (commandLine.getOptionValue("target-ip") == null) { + throw new IllegalStateException("as.sh is too old to support web console, " + + "please run the following command to upgrade to latest version:" + + "\ncurl -sLk http://arthas.io/arthas/install.sh | sh"); + } + configure.setIp((String) commandLine.getOptionValue("target-ip")); + configure.setTelnetPort((Integer) commandLine.getOptionValue("telnet-port")); + configure.setHttpPort((Integer) commandLine.getOptionValue("http-port")); + return configure; + } + + private void attachAgent(Configure configure) throws Exception { + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + Class vmdClass = loader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor"); + Class vmClass = loader.loadClass("com.sun.tools.attach.VirtualMachine"); + + Object attachVmdObj = null; + for (Object obj : (List) vmClass.getMethod("list", (Class[]) null).invoke(null, (Object[]) null)) { + Object pid = vmdClass.getMethod("id", (Class[]) null).invoke(obj, (Object[]) null); + if (pid.equals(Integer.toString(configure.getJavaPid()))) { + attachVmdObj = obj; + } + } + + Object vmObj = null; + try { + if (null == attachVmdObj) { // 使用 attach(String pid) 这种方式 + vmObj = vmClass.getMethod("attach", String.class).invoke(null, "" + configure.getJavaPid()); + } else { + vmObj = vmClass.getMethod("attach", vmdClass).invoke(null, attachVmdObj); + } + Method loadAgent = vmClass.getMethod("loadAgent", String.class, String.class); + loadAgent.invoke(vmObj, configure.getArthasAgent(), configure.getArthasCore() + ";" + configure.toString()); + } finally { + if (null != vmObj) { + vmClass.getMethod("detach", (Class[]) null).invoke(vmObj, (Object[]) null); + } + } + } + + + public static void main(String[] args) { + try { + new Arthas(args); + } catch (Throwable t) { + System.err.println("Start arthas failed, exception stack trace: "); + t.printStackTrace(); + System.exit(-1); + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/GlobalOptions.java b/core/src/main/java/com/taobao/arthas/core/GlobalOptions.java new file mode 100644 index 00000000..2905a1d9 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/GlobalOptions.java @@ -0,0 +1,102 @@ +package com.taobao.arthas.core; + +/** + * 全局开关 + * Created by vlinux on 15/6/4. + */ +public class GlobalOptions { + + /** + * 是否支持系统类
+ * 这个开关打开之后将能代理到来自JVM的部分类,由于有非常强的安全风险可能会引起系统崩溃
+ * 所以这个开关默认是关闭的,除非你非常了解你要做什么,否则请不要打开 + */ + @Option(level = 0, + name = "unsafe", + summary = "Option to support system-level class", + description = + "This option enables to proxy functionality of JVM classes." + + " Due to serious security risk a JVM crash is possibly be introduced." + + " Do not activate it unless you are able to manage." + ) + public static volatile boolean isUnsafe = false; + + /** + * 是否支持dump被增强的类
+ * 这个开关打开这后,每次增强类的时候都将会将增强的类dump到文件中,以便于进行反编译分析 + */ + @Option(level = 1, + name = "dump", + summary = "Option to dump the enhanced classes", + description = + "This option enables the enhanced classes to be dumped to external file " + + "for further de-compilation and analysis." + ) + public static volatile boolean isDump = false; + + /** + * 是否支持批量增强
+ * 这个开关打开后,每次均是批量增强类 + */ + @Option(level = 1, + name = "batch-re-transform", + summary = "Option to support batch reTransform Class", + description = "This options enables to reTransform classes with batch mode." + ) + public static volatile boolean isBatchReTransform = true; + + /** + * 是否支持json格式化输出
+ * 这个开关打开后,使用json格式输出目标对象,配合-x参数使用 + */ + @Option(level = 2, + name = "json-format", + summary = "Option to support JSON format of object output", + description = "This option enables to format object output with JSON when -x option selected." + ) + public static volatile boolean isUsingJson = false; + + /** + * 是否关闭子类 + */ + @Option( + level = 1, + name = "disable-sub-class", + summary = "Option to control include sub class when class matching", + description = "This option disable to include sub class when matching class." + ) + public static volatile boolean isDisableSubClass = false; + + /** + * 是否在asm中输出 + */ + @Option(level = 1, + name = "debug-for-asm", + summary = "Option to print DEBUG message if ASM is involved", + description = "This option enables to print DEBUG message of ASM for each method invocation." + ) + public static volatile boolean isDebugForAsm = false; + + /** + * 是否日志中保存命令执行结果 + */ + @Option(level = 1, + name = "save-result", + summary = "Option to print command's result to log file", + description = "This option enables to save each command's result to log file, " + + "which path is ${user.home}/logs/arthas/result.log." + ) + public static volatile boolean isSaveResult = false; + + /** + * job的超时时间 + */ + @Option(level = 2, + name = "job-timeout", + summary = "Option to job timeout", + description = "This option setting job timeout,The unit can be d, h, m, s for day, hour, minute, second. " + + "1d is one day in default" + ) + public static volatile String jobTimeout = "1d"; + +} diff --git a/core/src/main/java/com/taobao/arthas/core/Option.java b/core/src/main/java/com/taobao/arthas/core/Option.java new file mode 100644 index 00000000..613c9450 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/Option.java @@ -0,0 +1,35 @@ +package com.taobao.arthas.core; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Arthas全局选项 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Option { + + /* + * 选项级别,数字越小级别越高 + */ + int level(); + + /* + * 选项名称 + */ + String name(); + + /* + * 选项摘要说明 + */ + String summary(); + + /* + * 命令描述 + */ + String description(); + +} \ No newline at end of file diff --git a/core/src/main/java/com/taobao/arthas/core/advisor/Advice.java b/core/src/main/java/com/taobao/arthas/core/advisor/Advice.java new file mode 100644 index 00000000..55cca86c --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/advisor/Advice.java @@ -0,0 +1,150 @@ +package com.taobao.arthas.core.advisor; + +/** + * 通知点 Created by vlinux on 15/5/20. + */ +public class Advice { + + private final ClassLoader loader; + private final Class clazz; + private final ArthasMethod method; + private final Object target; + private final Object[] params; + private final Object returnObj; + private final Throwable throwExp; + + private final static int ACCESS_BEFORE = 1; + private final static int ACCESS_AFTER_RETUNING = 1 << 1; + private final static int ACCESS_AFTER_THROWING = 1 << 2; + + private final boolean isBefore; + private final boolean isThrow; + private final boolean isReturn; + + public boolean isBefore() { + return isBefore; + } + + public boolean isAfterReturning() { + return isReturn; + } + + public boolean isAfterThrowing() { + return isThrow; + } + + public ClassLoader getLoader() { + return loader; + } + + public Object getTarget() { + return target; + } + + public Object[] getParams() { + return params; + } + + public Object getReturnObj() { + return returnObj; + } + + public Throwable getThrowExp() { + return throwExp; + } + + public Class getClazz() { + return clazz; + } + + public ArthasMethod getMethod() { + return method; + } + + /** + * for finish + * + * @param loader 类加载器 + * @param clazz 类 + * @param method 方法 + * @param target 目标类 + * @param params 调用参数 + * @param returnObj 返回值 + * @param throwExp 抛出异常 + * @param access 进入场景 + */ + private Advice( + ClassLoader loader, + Class clazz, + ArthasMethod method, + Object target, + Object[] params, + Object returnObj, + Throwable throwExp, + int access) { + this.loader = loader; + this.clazz = clazz; + this.method = method; + this.target = target; + this.params = params; + this.returnObj = returnObj; + this.throwExp = throwExp; + isBefore = (access & ACCESS_BEFORE) == ACCESS_BEFORE; + isThrow = (access & ACCESS_AFTER_THROWING) == ACCESS_AFTER_THROWING; + isReturn = (access & ACCESS_AFTER_RETUNING) == ACCESS_AFTER_RETUNING; + } + + public static Advice newForBefore(ClassLoader loader, + Class clazz, + ArthasMethod method, + Object target, + Object[] params) { + return new Advice( + loader, + clazz, + method, + target, + params, + null, //returnObj + null, //throwExp + ACCESS_BEFORE + ); + } + + public static Advice newForAfterRetuning(ClassLoader loader, + Class clazz, + ArthasMethod method, + Object target, + Object[] params, + Object returnObj) { + return new Advice( + loader, + clazz, + method, + target, + params, + returnObj, + null, //throwExp + ACCESS_AFTER_RETUNING + ); + } + + public static Advice newForAfterThrowing(ClassLoader loader, + Class clazz, + ArthasMethod method, + Object target, + Object[] params, + Throwable throwExp) { + return new Advice( + loader, + clazz, + method, + target, + params, + null, //returnObj + throwExp, + ACCESS_AFTER_THROWING + ); + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/advisor/AdviceListener.java b/core/src/main/java/com/taobao/arthas/core/advisor/AdviceListener.java new file mode 100644 index 00000000..28d322ed --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/advisor/AdviceListener.java @@ -0,0 +1,74 @@ +package com.taobao.arthas.core.advisor; + +/** + * 通知监听器
+ * Created by vlinux on 15/5/17. + */ +public interface AdviceListener { + + /** + * 监听器创建
+ * 监听器被注册时触发 + */ + void create(); + + /** + * 监听器销毁
+ * 监听器被销毁时触发 + */ + void destroy(); + + /** + * 前置通知 + * + * @param loader 类加载器 + * @param className 类名 + * @param methodName 方法名 + * @param methodDesc 方法描述 + * @param target 目标类实例 + * 若目标为静态方法,则为null + * @param args 参数列表 + * @throws Throwable 通知过程出错 + */ + void before( + ClassLoader loader, String className, String methodName, String methodDesc, + Object target, Object[] args) throws Throwable; + + /** + * 返回通知 + * + * @param loader 类加载器 + * @param className 类名 + * @param methodName 方法名 + * @param methodDesc 方法描述 + * @param target 目标类实例 + * 若目标为静态方法,则为null + * @param args 参数列表 + * @param returnObject 返回结果 + * 若为无返回值方法(void),则为null + * @throws Throwable 通知过程出错 + */ + void afterReturning( + ClassLoader loader, String className, String methodName, String methodDesc, + Object target, Object[] args, + Object returnObject) throws Throwable; + + /** + * 异常通知 + * + * @param loader 类加载器 + * @param className 类名 + * @param methodName 方法名 + * @param methodDesc 方法描述 + * @param target 目标类实例 + * 若目标为静态方法,则为null + * @param args 参数列表 + * @param throwable 目标异常 + * @throws Throwable 通知过程出错 + */ + void afterThrowing( + ClassLoader loader, String className, String methodName, String methodDesc, + Object target, Object[] args, + Throwable throwable) throws Throwable; + +} diff --git a/core/src/main/java/com/taobao/arthas/core/advisor/AdviceListenerAdapter.java b/core/src/main/java/com/taobao/arthas/core/advisor/AdviceListenerAdapter.java new file mode 100644 index 00000000..d291c4b3 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/advisor/AdviceListenerAdapter.java @@ -0,0 +1,43 @@ +package com.taobao.arthas.core.advisor; + +/** + * 通知监听适配器 + */ +public class AdviceListenerAdapter implements AdviceListener { + + + @Override + public void create() { + + } + + @Override + public void destroy() { + + } + + @Override + public void before( + ClassLoader loader, String className, String methodName, String methodDesc, + Object target, Object[] args) throws Throwable { + + } + + @Override + public void afterReturning( + ClassLoader loader, String className, String methodName, String methodDesc, + Object target, Object[] args, + Object returnObject) throws Throwable { + + } + + @Override + public void afterThrowing( + ClassLoader loader, String className, String methodName, String methodDesc, + Object target, Object[] args, + Throwable throwable) throws Throwable { + + } + +} + diff --git a/core/src/main/java/com/taobao/arthas/core/advisor/AdviceWeaver.java b/core/src/main/java/com/taobao/arthas/core/advisor/AdviceWeaver.java new file mode 100644 index 00000000..8ab3a307 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/advisor/AdviceWeaver.java @@ -0,0 +1,984 @@ +package com.taobao.arthas.core.advisor; + +import com.taobao.arthas.core.GlobalOptions; +import com.taobao.arthas.core.util.matcher.Matcher; +import com.taobao.arthas.core.util.*; +import com.taobao.arthas.core.util.affect.EnhancerAffect; +import com.taobao.arthas.core.util.collection.GaStack; +import com.taobao.arthas.core.util.collection.ThreadUnsafeFixGaStack; +import com.taobao.arthas.core.util.collection.ThreadUnsafeGaStack; +import com.taobao.middleware.logger.Logger; +import org.objectweb.asm.*; +import org.objectweb.asm.commons.AdviceAdapter; +import org.objectweb.asm.commons.JSRInlinerAdapter; +import org.objectweb.asm.commons.Method; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 通知编织者
+ *

+ *

线程帧栈与执行帧栈

+ * 编织者在执行通知的时候有两个重要的栈:线程帧栈(threadFrameStack),执行帧栈(frameStack) + *

+ * Created by vlinux on 15/5/17. + */ +public class AdviceWeaver extends ClassVisitor implements Opcodes { + + private final static Logger logger = LogUtil.getArthasLogger(); + + + + // 线程帧栈堆栈大小 + private final static int FRAME_STACK_SIZE = 7; + // 通知监听器集合 + private final static Map advices + = new ConcurrentHashMap(); + // 线程帧封装 + private static final ThreadLocal>> threadBoundContext + = new ThreadLocal>>(); + // 防止自己递归调用 + private static final ThreadLocal isSelfCallRef = new ThreadLocal() { + + @Override + protected Boolean initialValue() { + return false; + } + + }; + + + /** + * 方法开始
+ * 用于编织通知器,外部不会直接调用 + * + * @param loader 类加载器 + * @param adviceId 通知ID + * @param className 类名 + * @param methodName 方法名 + * @param methodDesc 方法描述 + * @param target 返回结果 + * 若为无返回值方法(void),则为null + * @param args 参数列表 + */ + public static void methodOnBegin( + int adviceId, + ClassLoader loader, String className, String methodName, String methodDesc, + Object target, Object[] args) { + + if (isSelfCallRef.get()) { + return; + } else { + isSelfCallRef.set(true); + } + + try { + // 构建执行帧栈,保护当前的执行现场 + final GaStack frameStack = new ThreadUnsafeFixGaStack(FRAME_STACK_SIZE); + frameStack.push(loader); + frameStack.push(className); + frameStack.push(methodName); + frameStack.push(methodDesc); + frameStack.push(target); + frameStack.push(args); + + final AdviceListener listener = getListener(adviceId); + frameStack.push(listener); + + // 获取通知器并做前置通知 + before(listener, loader, className, methodName, methodDesc, target, args); + + // 保护当前执行帧栈,压入线程帧栈 + threadFrameStackPush(frameStack); + } finally { + isSelfCallRef.set(false); + } + + } + + + /** + * 方法以返回结束
+ * 用于编织通知器,外部不会直接调用 + * + * @param returnObject 返回对象 + * 若目标为静态方法,则为null + */ + public static void methodOnReturnEnd(Object returnObject) { + methodOnEnd(false, returnObject); + } + + /** + * 方法以抛异常结束
+ * 用于编织通知器,外部不会直接调用 + * + * @param throwable 抛出异常 + */ + public static void methodOnThrowingEnd(Throwable throwable) { + methodOnEnd(true, throwable); + } + + /** + * 所有的返回都统一处理 + * + * @param isThrowing 标记正常返回结束还是抛出异常结束 + * @param returnOrThrowable 正常返回或者抛出异常对象 + */ + private static void methodOnEnd(boolean isThrowing, Object returnOrThrowable) { + + if (isSelfCallRef.get()) { + return; + } else { + isSelfCallRef.set(true); + } + + try { + // 弹射线程帧栈,恢复Begin所保护的执行帧栈 + final GaStack frameStack = threadFrameStackPop(); + + // 弹射执行帧栈,恢复Begin所保护的现场 + final AdviceListener listener = (AdviceListener) frameStack.pop(); + final Object[] args = (Object[]) frameStack.pop(); + final Object target = frameStack.pop(); + final String methodDesc = (String) frameStack.pop(); + final String methodName = (String) frameStack.pop(); + final String className = (String) frameStack.pop(); + final ClassLoader loader = (ClassLoader) frameStack.pop(); + + // 异常通知 + if (isThrowing) { + afterThrowing(listener, loader, className, methodName, methodDesc, target, args, (Throwable) returnOrThrowable); + } + + // 返回通知 + else { + afterReturning(listener, loader, className, methodName, methodDesc, target, args, returnOrThrowable); + } + } finally { + isSelfCallRef.set(false); + } + + } + + /** + * 方法内部调用开始 + * + * @param adviceId 通知ID + * @param owner 调用类名 + * @param name 调用方法名 + * @param desc 调用方法描述 + */ + public static void methodOnInvokeBeforeTracing(int adviceId, String owner, String name, String desc) { + final InvokeTraceable listener = (InvokeTraceable) getListener(adviceId); + if (null != listener) { + try { + listener.invokeBeforeTracing(owner, name, desc); + } catch (Throwable t) { + logger.warn("advice before tracing failed.", t); + } + } + } + + /** + * 方法内部调用结束(正常返回) + * + * @param adviceId 通知ID + * @param owner 调用类名 + * @param name 调用方法名 + * @param desc 调用方法描述 + */ + public static void methodOnInvokeAfterTracing(int adviceId, String owner, String name, String desc) { + final InvokeTraceable listener = (InvokeTraceable) getListener(adviceId); + if (null != listener) { + try { + listener.invokeAfterTracing(owner, name, desc); + } catch (Throwable t) { + logger.warn("advice after tracing failed.", t); + } + } + } + + /** + * 方法内部调用结束(异常返回) + * + * @param adviceId 通知ID + * @param owner 调用类名 + * @param name 调用方法名 + * @param desc 调用方法描述 + */ + public static void methodOnInvokeThrowTracing(int adviceId, String owner, String name, String desc) { + final InvokeTraceable listener = (InvokeTraceable) getListener(adviceId); + if (null != listener) { + try { + listener.invokeThrowTracing(owner, name, desc); + } catch (Throwable t) { + logger.warn("advice throw tracing failed.", t); + } + } + } + + /* + * 线程帧栈压栈
+ * 将当前执行帧栈压入线程栈 + */ + private static void threadFrameStackPush(GaStack frameStack) { + GaStack> threadFrameStack = threadBoundContext.get(); + if (null == threadFrameStack) { + threadBoundContext.set(threadFrameStack = new ThreadUnsafeGaStack>()); + } + + threadFrameStack.push(frameStack); + } + + private static GaStack threadFrameStackPop() { + return threadBoundContext.get().pop(); + } + + private static AdviceListener getListener(int adviceId) { + return advices.get(adviceId); + } + + /** + * 注册监听器 + * + * @param adviceId 通知ID + * @param listener 通知监听器 + */ + public static void reg(int adviceId, AdviceListener listener) { + + // 触发监听器创建 + listener.create(); + + // 注册监听器 + advices.put(adviceId, listener); + } + + /** + * 注销监听器 + * + * @param adviceId 通知ID + */ + public static void unReg(int adviceId) { + + // 注销监听器 + final AdviceListener listener = advices.remove(adviceId); + + // 触发监听器销毁 + if (null != listener) { + listener.destroy(); + } + + } + + + /** + * 恢复监听 + * + * @param adviceId 通知ID + * @param listener 通知监听器 + */ + public static void resume(int adviceId, AdviceListener listener) { + // 注册监听器 + advices.put(adviceId, listener); + } + + /** + * 暂停监听 + * + * @param adviceId 通知ID + */ + public static AdviceListener suspend(int adviceId) { + // 注销监听器 + return advices.remove(adviceId); + } + + private static void before(AdviceListener listener, + ClassLoader loader, String className, String methodName, String methodDesc, + Object target, Object[] args) { + + if (null != listener) { + try { + listener.before(loader, className, methodName, methodDesc, target, args); + } catch (Throwable t) { + logger.warn("advice before failed.", t); + } + } + + } + + private static void afterReturning(AdviceListener listener, + ClassLoader loader, String className, String methodName, String methodDesc, + Object target, Object[] args, Object returnObject) { + if (null != listener) { + try { + listener.afterReturning(loader, className, methodName, methodDesc, target, args, returnObject); + } catch (Throwable t) { + logger.warn("advice returning failed.", t); + } + } + } + + private static void afterThrowing(AdviceListener listener, + ClassLoader loader, String className, String methodName, String methodDesc, + Object target, Object[] args, Throwable throwable) { + if (null != listener) { + try { + listener.afterThrowing(loader, className, methodName, methodDesc, target, args, throwable); + } catch (Throwable t) { + logger.warn("advice throwing failed.", t); + } + } + } + + + private final int adviceId; + private final boolean isTracing; + private final boolean skipJDKTrace; + private final String className; + private String superName; + private final Matcher matcher; + private final EnhancerAffect affect; + + + /** + * 构建通知编织器 + * + * @param adviceId 通知ID + * @param isTracing 可跟踪方法调用 + * @param className 类名称 + * @param matcher 方法匹配 + * 只有匹配上的方法才会被织入通知器 + * @param affect 影响计数 + * @param cv ClassVisitor for ASM + */ + public AdviceWeaver(int adviceId, boolean isTracing, boolean skipJDKTrace, String className, Matcher matcher, EnhancerAffect affect, ClassVisitor cv) { + super(ASM5, cv); + this.adviceId = adviceId; + this.isTracing = isTracing; + this.skipJDKTrace = skipJDKTrace; + this.className = className; + this.matcher = matcher; + this.affect = affect; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + super.visit(version, access, name, signature, superName, interfaces); + this.superName = superName; + } + + protected boolean isSuperOrSiblingConstructorCall(int opcode, String owner, String name) { + return (opcode == Opcodes.INVOKESPECIAL && name.equals("") + && (superName.equals(owner) || className.equals(owner))); + } + + /** + * 是否抽象属性 + */ + private boolean isAbstract(int access) { + return (ACC_ABSTRACT & access) == ACC_ABSTRACT; + } + + + /** + * 是否需要忽略 + */ + private boolean isIgnore(MethodVisitor mv, int access, String methodName) { + return null == mv + || isAbstract(access) + || !matcher.matching(methodName) + || ArthasCheckUtils.isEquals(methodName, ""); + } + + @Override + public MethodVisitor visitMethod( + final int access, + final String name, + final String desc, + final String signature, + final String[] exceptions) { + + final MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); + + if (isIgnore(mv, access, name)) { + return mv; + } + + // 编织方法计数 + affect.mCnt(1); + + return new AdviceAdapter(ASM5, new JSRInlinerAdapter(mv, access, name, desc, signature, exceptions), access, name, desc) { + + // -- Label for try...catch block + private final Label beginLabel = new Label(); + private final Label endLabel = new Label(); + + // -- KEY of advice -- + private final int KEY_ARTHAS_ADVICE_BEFORE_METHOD = 0; + private final int KEY_ARTHAS_ADVICE_RETURN_METHOD = 1; + private final int KEY_ARTHAS_ADVICE_THROWS_METHOD = 2; + private final int KEY_ARTHAS_ADVICE_BEFORE_INVOKING_METHOD = 3; + private final int KEY_ARTHAS_ADVICE_AFTER_INVOKING_METHOD = 4; + private final int KEY_ARTHAS_ADVICE_THROW_INVOKING_METHOD = 5; + + + // -- KEY of ASM_TYPE or ASM_METHOD -- + private final Type ASM_TYPE_SPY = Type.getType("Ljava/arthas/Spy;"); + private final Type ASM_TYPE_OBJECT = Type.getType(Object.class); + private final Type ASM_TYPE_OBJECT_ARRAY = Type.getType(Object[].class); + private final Type ASM_TYPE_CLASS = Type.getType(Class.class); + private final Type ASM_TYPE_INTEGER = Type.getType(Integer.class); + private final Type ASM_TYPE_CLASS_LOADER = Type.getType(ClassLoader.class); + private final Type ASM_TYPE_STRING = Type.getType(String.class); + private final Type ASM_TYPE_THROWABLE = Type.getType(Throwable.class); + private final Type ASM_TYPE_INT = Type.getType(int.class); + private final Type ASM_TYPE_METHOD = Type.getType(java.lang.reflect.Method.class); + private final Method ASM_METHOD_METHOD_INVOKE = Method.getMethod("Object invoke(Object,Object[])"); + + // 代码锁 + private final CodeLock codeLockForTracing = new TracingAsmCodeLock(this); + + + private void _debug(final StringBuilder append, final String msg) { + + if (!GlobalOptions.isDebugForAsm) { + return; + } + + // println msg + visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); + if (StringUtils.isBlank(append.toString())) { + visitLdcInsn(append.append(msg).toString()); + } else { + visitLdcInsn(append.append(" >> ").append(msg).toString()); + } + + visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); + } + +// private void _debug_dup(final String msg) { +// +// if (!isDebugForAsm) { +// return; +// } +// +// // print prefix +// visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); +// visitLdcInsn(msg); +// visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "print", "(Ljava/lang/String;)V", false); +// +// // println msg +// dup(); +// visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); +// swap(); +// visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "toString", "()Ljava/lang/String;", false); +// visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); +// } + + /** + * 加载通知方法 + * @param keyOfMethod 通知方法KEY + */ + private void loadAdviceMethod(int keyOfMethod) { + + switch (keyOfMethod) { + + case KEY_ARTHAS_ADVICE_BEFORE_METHOD: { + getStatic(ASM_TYPE_SPY, "ON_BEFORE_METHOD", ASM_TYPE_METHOD); + break; + } + + case KEY_ARTHAS_ADVICE_RETURN_METHOD: { + getStatic(ASM_TYPE_SPY, "ON_RETURN_METHOD", ASM_TYPE_METHOD); + break; + } + + case KEY_ARTHAS_ADVICE_THROWS_METHOD: { + getStatic(ASM_TYPE_SPY, "ON_THROWS_METHOD", ASM_TYPE_METHOD); + break; + } + + case KEY_ARTHAS_ADVICE_BEFORE_INVOKING_METHOD: { + getStatic(ASM_TYPE_SPY, "BEFORE_INVOKING_METHOD", ASM_TYPE_METHOD); + break; + } + + case KEY_ARTHAS_ADVICE_AFTER_INVOKING_METHOD: { + getStatic(ASM_TYPE_SPY, "AFTER_INVOKING_METHOD", ASM_TYPE_METHOD); + break; + } + + case KEY_ARTHAS_ADVICE_THROW_INVOKING_METHOD: { + getStatic(ASM_TYPE_SPY, "THROW_INVOKING_METHOD", ASM_TYPE_METHOD); + break; + } + + default: { + throw new IllegalArgumentException("illegal keyOfMethod=" + keyOfMethod); + } + + } + + } + + /** + * 加载ClassLoader
+ * 这里分开静态方法中ClassLoader的获取以及普通方法中ClassLoader的获取 + * 主要是性能上的考虑 + */ + private void loadClassLoader() { + + if (this.isStaticMethod()) { + visitLdcInsn(StringUtils.normalizeClassName(className)); + invokeStatic(ASM_TYPE_CLASS, Method.getMethod("Class forName(String)")); + invokeVirtual(ASM_TYPE_CLASS, Method.getMethod("ClassLoader getClassLoader()")); + + } else { + loadThis(); + invokeVirtual(ASM_TYPE_OBJECT, Method.getMethod("Class getClass()")); + invokeVirtual(ASM_TYPE_CLASS, Method.getMethod("ClassLoader getClassLoader()")); + } + + } + + /** + * 加载before通知参数数组 + */ + private void loadArrayForBefore() { + push(7); + newArray(ASM_TYPE_OBJECT); + + dup(); + push(0); + push(adviceId); + box(ASM_TYPE_INT); + arrayStore(ASM_TYPE_INTEGER); + + dup(); + push(1); + loadClassLoader(); + arrayStore(ASM_TYPE_CLASS_LOADER); + + dup(); + push(2); + push(className); + arrayStore(ASM_TYPE_STRING); + + dup(); + push(3); + push(name); + arrayStore(ASM_TYPE_STRING); + + dup(); + push(4); + push(desc); + arrayStore(ASM_TYPE_STRING); + + dup(); + push(5); + loadThisOrPushNullIfIsStatic(); + arrayStore(ASM_TYPE_OBJECT); + + dup(); + push(6); + loadArgArray(); + arrayStore(ASM_TYPE_OBJECT_ARRAY); + } + + + @Override + protected void onMethodEnter() { + + codeLockForTracing.lock(new CodeLock.Block() { + @Override + public void code() { + + final StringBuilder append = new StringBuilder(); + _debug(append, "debug:onMethodEnter()"); + + // 加载before方法 + loadAdviceMethod(KEY_ARTHAS_ADVICE_BEFORE_METHOD); + + _debug(append, "debug:onMethodEnter() > loadAdviceMethod()"); + + // 推入Method.invoke()的第一个参数 + pushNull(); + + // 方法参数 + loadArrayForBefore(); + + _debug(append, "debug:onMethodEnter() > loadAdviceMethod() > loadArrayForBefore()"); + + // 调用方法 + invokeVirtual(ASM_TYPE_METHOD, ASM_METHOD_METHOD_INVOKE); + pop(); + + _debug(append, "debug:onMethodEnter() > loadAdviceMethod() > loadArrayForBefore() > invokeVirtual()"); + } + }); + + mark(beginLabel); + + } + + + /* + * 加载return通知参数数组 + */ + private void loadReturnArgs() { + dup2X1(); + pop2(); + push(1); + newArray(ASM_TYPE_OBJECT); + dup(); + dup2X1(); + pop2(); + push(0); + swap(); + arrayStore(ASM_TYPE_OBJECT); + } + + @Override + protected void onMethodExit(final int opcode) { + + if (!isThrow(opcode)) { + codeLockForTracing.lock(new CodeLock.Block() { + @Override + public void code() { + + final StringBuilder append = new StringBuilder(); + _debug(append, "debug:onMethodExit()"); + + // 加载返回对象 + loadReturn(opcode); + _debug(append, "debug:onMethodExit() > loadReturn()"); + + + // 加载returning方法 + loadAdviceMethod(KEY_ARTHAS_ADVICE_RETURN_METHOD); + _debug(append, "debug:onMethodExit() > loadReturn() > loadAdviceMethod()"); + + // 推入Method.invoke()的第一个参数 + pushNull(); + + // 加载return通知参数数组 + loadReturnArgs(); + _debug(append, "debug:onMethodExit() > loadReturn() > loadAdviceMethod() > loadReturnArgs()"); + + invokeVirtual(ASM_TYPE_METHOD, ASM_METHOD_METHOD_INVOKE); + pop(); + + _debug(append, "debug:onMethodExit() > loadReturn() > loadAdviceMethod() > loadReturnArgs() > invokeVirtual()"); + } + }); + } + + } + + + /* + * 创建throwing通知参数本地变量 + */ + private void loadThrowArgs() { + dup2X1(); + pop2(); + push(1); + newArray(ASM_TYPE_OBJECT); + dup(); + dup2X1(); + pop2(); + push(0); + swap(); + arrayStore(ASM_TYPE_THROWABLE); + } + + @Override + public void visitMaxs(int maxStack, int maxLocals) { + + mark(endLabel); +// catchException(beginLabel, endLabel, ASM_TYPE_THROWABLE); + visitTryCatchBlock(beginLabel, endLabel, mark(), + ASM_TYPE_THROWABLE.getInternalName()); + + codeLockForTracing.lock(new CodeLock.Block() { + @Override + public void code() { + + final StringBuilder append = new StringBuilder(); + _debug(append, "debug:catchException()"); + + // 加载异常 + loadThrow(); + _debug(append, "debug:catchException() > loadThrow() > loadAdviceMethod()"); + + // 加载throwing方法 + loadAdviceMethod(KEY_ARTHAS_ADVICE_THROWS_METHOD); + _debug(append, "debug:catchException() > loadThrow() > loadAdviceMethod()"); + + + // 推入Method.invoke()的第一个参数 + pushNull(); + + // 加载throw通知参数数组 + loadThrowArgs(); + _debug(append, "debug:catchException() > loadThrow() > loadAdviceMethod() > loadThrowArgs()"); + + // 调用方法 + invokeVirtual(ASM_TYPE_METHOD, ASM_METHOD_METHOD_INVOKE); + pop(); + _debug(append, "debug:catchException() > loadThrow() > loadAdviceMethod() > loadThrowArgs() > invokeVirtual()"); + + } + }); + + throwException(); + + super.visitMaxs(maxStack, maxLocals); + } + + /** + * 是否静态方法 + * @return true:静态方法 / false:非静态方法 + */ + private boolean isStaticMethod() { + return (methodAccess & ACC_STATIC) != 0; + } + + /** + * 是否抛出异常返回(通过字节码判断) + * @param opcode 操作码 + * @return true:以抛异常形式返回 / false:非抛异常形式返回(return) + */ + private boolean isThrow(int opcode) { + return opcode == ATHROW; + } + + /** + * 将NULL推入堆栈 + */ + private void pushNull() { + push((Type) null); + } + + /** + * 加载this/null + */ + private void loadThisOrPushNullIfIsStatic() { + if (isStaticMethod()) { + pushNull(); + } else { + loadThis(); + } + } + + /** + * 加载返回值 + * @param opcode 操作吗 + */ + private void loadReturn(int opcode) { + switch (opcode) { + + case RETURN: { + pushNull(); + break; + } + + case ARETURN: { + dup(); + break; + } + + case LRETURN: + case DRETURN: { + dup2(); + box(Type.getReturnType(methodDesc)); + break; + } + + default: { + dup(); + box(Type.getReturnType(methodDesc)); + break; + } + + } + } + + /** + * 加载异常 + */ + private void loadThrow() { + dup(); + } + + + /** + * 加载方法调用跟踪通知所需参数数组 + */ + private void loadArrayForInvokeTracing(String owner, String name, String desc) { + push(4); + newArray(ASM_TYPE_OBJECT); + + dup(); + push(0); + push(adviceId); + box(ASM_TYPE_INT); + arrayStore(ASM_TYPE_INTEGER); + + dup(); + push(1); + push(owner); + arrayStore(ASM_TYPE_STRING); + + dup(); + push(2); + push(name); + arrayStore(ASM_TYPE_STRING); + + dup(); + push(3); + push(desc); + arrayStore(ASM_TYPE_STRING); + } + + + @Override + public void visitInsn(int opcode) { + super.visitInsn(opcode); + codeLockForTracing.code(opcode); + } + + @Override + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + tcbs.add(new AsmTryCatchBlock(start, end, handler, type)); + } + + List tcbs = new ArrayList(); + + @Override + public void visitEnd() { + for (AsmTryCatchBlock tcb : tcbs) { + super.visitTryCatchBlock(tcb.start, tcb.end, tcb.handler, tcb.type); + } + + super.visitEnd(); + } + + /* + * 跟踪代码 + */ + private void tracing(final int tracingType, final String owner, final String name, final String desc) { + + final String label; + switch (tracingType) { + case KEY_ARTHAS_ADVICE_BEFORE_INVOKING_METHOD: { + label = "beforeInvoking"; + break; + } + case KEY_ARTHAS_ADVICE_AFTER_INVOKING_METHOD: { + label = "afterInvoking"; + break; + } + case KEY_ARTHAS_ADVICE_THROW_INVOKING_METHOD: { + label = "throwInvoking"; + break; + } + default: { + throw new IllegalStateException("illegal tracing type: " + tracingType); + } + } + + codeLockForTracing.lock(new CodeLock.Block() { + @Override + public void code() { + + final StringBuilder append = new StringBuilder(); + _debug(append, "debug:" + label + "()"); + + loadAdviceMethod(tracingType); + _debug(append, "loadAdviceMethod()"); + + pushNull(); + loadArrayForInvokeTracing(owner, name, desc); + _debug(append, "loadArrayForInvokeTracing()"); + + invokeVirtual(ASM_TYPE_METHOD, ASM_METHOD_METHOD_INVOKE); + pop(); + _debug(append, "invokeVirtual()"); + + } + }); + + } + + @Override + public void visitMethodInsn(int opcode, final String owner, final String name, final String desc, boolean itf) { + if (isSuperOrSiblingConstructorCall(opcode, owner, name)) { + super.visitMethodInsn(opcode, owner, name, desc, itf); + return; + } + + if (!isTracing || codeLockForTracing.isLock()) { + super.visitMethodInsn(opcode, owner, name, desc, itf); + return; + } + + //是否要对JDK内部的方法调用进行trace + if (skipJDKTrace && owner.startsWith("java/")) { + super.visitMethodInsn(opcode, owner, name, desc, itf); + return; + } + + // 方法调用前通知 + tracing(KEY_ARTHAS_ADVICE_BEFORE_INVOKING_METHOD, owner, name, desc); + + final Label beginLabel = new Label(); + final Label endLabel = new Label(); + final Label finallyLabel = new Label(); + + // try + // { + + mark(beginLabel); + super.visitMethodInsn(opcode, owner, name, desc, itf); + mark(endLabel); + + // 方法调用后通知 + tracing(KEY_ARTHAS_ADVICE_AFTER_INVOKING_METHOD, owner, name, desc); + goTo(finallyLabel); + + // } + // catch + // { + + catchException(beginLabel, endLabel, ASM_TYPE_THROWABLE); + tracing(KEY_ARTHAS_ADVICE_THROW_INVOKING_METHOD, owner, name, desc); + + throwException(); + + // } + // finally + // { + mark(finallyLabel); + // } + } + }; + } + + static class AsmTryCatchBlock { + Label start; + Label end; + Label handler; + String type; + + AsmTryCatchBlock(Label start, Label end, Label handler, String type) { + this.start = start; + this.end = end; + this.handler = handler; + this.type = type; + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/advisor/ArthasMethod.java b/core/src/main/java/com/taobao/arthas/core/advisor/ArthasMethod.java new file mode 100644 index 00000000..cd8f60fd --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/advisor/ArthasMethod.java @@ -0,0 +1,98 @@ +package com.taobao.arthas.core.advisor; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Arthas封装的方法
+ * 主要用来封装构造函数cinit/init/method + * Created by vlinux on 15/5/24. + */ +public class ArthasMethod { + + private final int type; + private final Constructor constructor; + private final Method method; + + /* + * 构造方法 + */ + private static final int TYPE_INIT = 1 << 1; + + /* + * 普通方法 + */ + private static final int TYPE_METHOD = 1 << 2; + + /** + * 是否构造方法 + * + * @return true/false + */ + public boolean isInit() { + return (TYPE_INIT & type) == TYPE_INIT; + } + + /** + * 是否普通方法 + * + * @return true/false + */ + public boolean isMethod() { + return (TYPE_METHOD & type) == TYPE_METHOD; + } + + /** + * 获取方法名称 + * + * @return 返回方法名称 + */ + public String getName() { + return isInit() + ? "" + : method.getName(); + } + + @Override + public String toString() { + return isInit() + ? constructor.toString() + : method.toString(); + } + + public boolean isAccessible() { + return isInit() + ? constructor.isAccessible() + : method.isAccessible(); + } + + public void setAccessible(boolean accessFlag) { + if (isInit()) { + constructor.setAccessible(accessFlag); + } else { + method.setAccessible(accessFlag); + } + } + + public Object invoke(Object target, Object... args) throws IllegalAccessException, InvocationTargetException, InstantiationException { + return isInit() + ? constructor.newInstance(args) + : method.invoke(target, args); + } + + private ArthasMethod(int type, Constructor constructor, Method method) { + this.type = type; + this.constructor = constructor; + this.method = method; + } + + public static ArthasMethod newInit(Constructor constructor) { + return new ArthasMethod(TYPE_INIT, constructor, null); + } + + public static ArthasMethod newMethod(Method method) { + return new ArthasMethod(TYPE_METHOD, null, method); + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/advisor/AsmCodeLock.java b/core/src/main/java/com/taobao/arthas/core/advisor/AsmCodeLock.java new file mode 100644 index 00000000..250233d8 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/advisor/AsmCodeLock.java @@ -0,0 +1,118 @@ +package com.taobao.arthas.core.advisor; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.commons.AdviceAdapter; + +/** + * ASM代码锁
+ * Created by vlinux on 15/5/28. + */ +public class AsmCodeLock implements CodeLock, Opcodes { + + private final AdviceAdapter aa; + + // 锁标记 + private boolean isLook; + + // 代码块开始特征数组 + private final int[] beginCodeArray; + + // 代码块结束特征数组 + private final int[] endCodeArray; + + // 代码匹配索引 + private int index = 0; + + + /** + * 用ASM构建代码锁 + * + * @param aa ASM + * @param beginCodeArray 代码块开始特征数组 + * 字节码流要求不能破坏执行堆栈 + * @param endCodeArray 代码块结束特征数组 + * 字节码流要求不能破坏执行堆栈 + */ + public AsmCodeLock(AdviceAdapter aa, int[] beginCodeArray, int[] endCodeArray) { + if (null == beginCodeArray + || null == endCodeArray + || beginCodeArray.length != endCodeArray.length) { + throw new IllegalArgumentException(); + } + + this.aa = aa; + this.beginCodeArray = beginCodeArray; + this.endCodeArray = endCodeArray; + + } + + @Override + public void code(int code) { + + final int[] codes = isLock() ? endCodeArray : beginCodeArray; + + if (index >= codes.length) { + reset(); + return; + } + + if (codes[index] != code) { + reset(); + return; + } + + if (++index == codes.length) { + // 翻转锁状态 + isLook = !isLook; + reset(); + } + + } + + /* + * 重置索引
+ * 一般在代码序列判断失败时,则会对索引进行重置,冲头开始匹配特征序列 + */ + private void reset() { + index = 0; + } + + + private void asm(int opcode) { + aa.visitInsn(opcode); + } + + /** + * 锁定序列 + */ + private void lock() { + for (int op : beginCodeArray) { + asm(op); + } + } + + /* + * 解锁序列 + */ + private void unLock() { + for (int op : endCodeArray) { + asm(op); + } + } + + @Override + public boolean isLock() { + return isLook; + } + + @Override + public void lock(Block block) { + lock(); + try { + block.code(); + } finally { + unLock(); + } + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/advisor/CodeLock.java b/core/src/main/java/com/taobao/arthas/core/advisor/CodeLock.java new file mode 100644 index 00000000..1095b775 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/advisor/CodeLock.java @@ -0,0 +1,43 @@ +package com.taobao.arthas.core.advisor; + +/** + * 代码锁
+ * 什么叫代码锁?代码锁的出现是由于在字节码中,我们无法用简单的if语句来判定这段代码是生成的还是原有的。 + * 这会导致一些监控逻辑的混乱,比如trace命令如果不使用代码锁保护,将能看到Arthas所植入的代码并进行跟踪 + * Created by vlinux on 15/5/28. + */ +public interface CodeLock { + + /** + * 根据字节码流锁或解锁代码
+ * 通过对字节码流的判断,决定当前代码是锁定和解锁 + * + * @param opcode 字节码 + */ + void code(int opcode); + + /** + * 判断当前代码是否还在锁定中 + * + * @return true/false + */ + boolean isLock(); + + /** + * 将一个代码块纳入代码锁保护范围 + * + * @param block 代码块 + */ + void lock(Block block); + + /** + * 代码块 + */ + interface Block { + /** + * 代码 + */ + void code(); + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/advisor/Enhancer.java b/core/src/main/java/com/taobao/arthas/core/advisor/Enhancer.java new file mode 100644 index 00000000..cbb993e4 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/advisor/Enhancer.java @@ -0,0 +1,399 @@ +package com.taobao.arthas.core.advisor; + +import com.taobao.arthas.core.GlobalOptions; +import com.taobao.arthas.core.util.Constants; +import com.taobao.arthas.core.util.FileUtils; +import com.taobao.arthas.core.util.LogUtil; +import com.taobao.arthas.core.util.matcher.Matcher; +import com.taobao.arthas.core.util.SearchUtils; +import com.taobao.arthas.core.util.affect.EnhancerAffect; + +import com.taobao.arthas.core.util.reflect.FieldUtils; +import com.taobao.middleware.logger.Logger; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; + +import java.io.File; +import java.io.IOException; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.lang.instrument.Instrumentation; +import java.lang.instrument.UnmodifiableClassException; +import java.lang.reflect.Method; +import java.security.ProtectionDomain; +import java.util.*; + +import static com.taobao.arthas.core.util.ArthasCheckUtils.isEquals; +import static java.lang.System.arraycopy; +import static org.objectweb.asm.ClassReader.EXPAND_FRAMES; +import static org.objectweb.asm.ClassWriter.COMPUTE_FRAMES; +import static org.objectweb.asm.ClassWriter.COMPUTE_MAXS; + +/** + * 对类进行通知增强 + * Created by vlinux on 15/5/17. + */ +public class Enhancer implements ClassFileTransformer { + + private static final Logger logger = LogUtil.getArthasLogger(); + + private final int adviceId; + private final boolean isTracing; + private final boolean skipJDKTrace; + private final Set> matchingClasses; + private final Matcher methodNameMatcher; + private final EnhancerAffect affect; + + // 类-字节码缓存 + private final static Map/*Class*/, byte[]/*bytes of Class*/> classBytesCache + = new WeakHashMap, byte[]>(); + + /** + * @param adviceId 通知编号 + * @param isTracing 可跟踪方法调用 + * @param matchingClasses 匹配中的类 + * @param methodNameMatcher 方法名匹配 + * @param affect 影响统计 + */ + private Enhancer(int adviceId, + boolean isTracing, + boolean skipJDKTrace, + Set> matchingClasses, + Matcher methodNameMatcher, + EnhancerAffect affect) { + this.adviceId = adviceId; + this.isTracing = isTracing; + this.skipJDKTrace = skipJDKTrace; + this.matchingClasses = matchingClasses; + this.methodNameMatcher = methodNameMatcher; + this.affect = affect; + } + + private void spy(final ClassLoader targetClassLoader) throws Exception { + if (targetClassLoader == null) { + // 增强JDK自带的类,targetClassLoader为null + return; + } + // 因为 Spy 是被bootstrap classloader加载的,所以一定可以被找到,如果找不到的话,说明应用方的classloader实现有问题 + Class spyClass = targetClassLoader.loadClass(Constants.SPY_CLASSNAME); + + final ClassLoader arthasClassLoader = Enhancer.class.getClassLoader(); + + // 初始化间谍, AgentLauncher会把各种hook设置到ArthasClassLoader当中 + // 这里我们需要把这些hook取出来设置到目标classloader当中 + Method initMethod = spyClass.getMethod("init", ClassLoader.class, Method.class, + Method.class, Method.class, Method.class, Method.class, Method.class); + initMethod.invoke(null, arthasClassLoader, + FieldUtils.getField(spyClass, "ON_BEFORE_METHOD").get(null), + FieldUtils.getField(spyClass, "ON_RETURN_METHOD").get(null), + FieldUtils.getField(spyClass, "ON_THROWS_METHOD").get(null), + FieldUtils.getField(spyClass, "BEFORE_INVOKING_METHOD").get(null), + FieldUtils.getField(spyClass, "AFTER_INVOKING_METHOD").get(null), + FieldUtils.getField(spyClass, "THROW_INVOKING_METHOD").get(null)); + } + + @Override + public byte[] transform( + final ClassLoader inClassLoader, + String className, + Class classBeingRedefined, + ProtectionDomain protectionDomain, + byte[] classfileBuffer) throws IllegalClassFormatException { + + + // 这里要再次过滤一次,为啥?因为在transform的过程中,有可能还会再诞生新的类 + // 所以需要将之前需要转换的类集合传递下来,再次进行判断 + if (!matchingClasses.contains(classBeingRedefined)) { + return null; + } + + final ClassReader cr; + + // 首先先检查是否在缓存中存在Class字节码 + // 因为要支持多人协作,存在多人同时增强的情况 + final byte[] byteOfClassInCache = classBytesCache.get(classBeingRedefined); + if (null != byteOfClassInCache) { + cr = new ClassReader(byteOfClassInCache); + } + + // 如果没有命中缓存,则从原始字节码开始增强 + else { + cr = new ClassReader(classfileBuffer); + } + + // 字节码增强 + final ClassWriter cw = new ClassWriter(cr, COMPUTE_FRAMES | COMPUTE_MAXS) { + + + /* + * 注意,为了自动计算帧的大小,有时必须计算两个类共同的父类。 + * 缺省情况下,ClassWriter将会在getCommonSuperClass方法中计算这些,通过在加载这两个类进入虚拟机时,使用反射API来计算。 + * 但是,如果你将要生成的几个类相互之间引用,这将会带来问题,因为引用的类可能还不存在。 + * 在这种情况下,你可以重写getCommonSuperClass方法来解决这个问题。 + * + * 通过重写 getCommonSuperClass() 方法,更正获取ClassLoader的方式,改成使用指定ClassLoader的方式进行。 + * 规避了原有代码采用Object.class.getClassLoader()的方式 + */ + @Override + protected String getCommonSuperClass(String type1, String type2) { + Class c, d; + final ClassLoader classLoader = inClassLoader; + try { + c = Class.forName(type1.replace('/', '.'), false, classLoader); + d = Class.forName(type2.replace('/', '.'), false, classLoader); + } catch (Exception e) { + throw new RuntimeException(e); + } + if (c.isAssignableFrom(d)) { + return type1; + } + if (d.isAssignableFrom(c)) { + return type2; + } + if (c.isInterface() || d.isInterface()) { + return "java/lang/Object"; + } else { + do { + c = c.getSuperclass(); + } while (!c.isAssignableFrom(d)); + return c.getName().replace('.', '/'); + } + } + + }; + + try { + + // 生成增强字节码 + cr.accept(new AdviceWeaver(adviceId, isTracing, skipJDKTrace, cr.getClassName(), methodNameMatcher, affect, cw), EXPAND_FRAMES); + final byte[] enhanceClassByteArray = cw.toByteArray(); + + // 生成成功,推入缓存 + classBytesCache.put(classBeingRedefined, enhanceClassByteArray); + + // dump the class + dumpClassIfNecessary(className, enhanceClassByteArray, affect); + + // 成功计数 + affect.cCnt(1); + + // 排遣间谍 + try { + spy(inClassLoader); + } catch (Throwable t) { + logger.warn("print spy failed. classname={};loader={};", className, inClassLoader, t); + throw t; + } + + return enhanceClassByteArray; + } catch (Throwable t) { + logger.warn("transform loader[{}]:class[{}] failed.", inClassLoader, className, t); + } + + return null; + } + + /** + * dump class to file + */ + private static void dumpClassIfNecessary(String className, byte[] data, EnhancerAffect affect) { + if (!GlobalOptions.isDump) { + return; + } + final File dumpClassFile = new File("./arthas-class-dump/" + className + ".class"); + final File classPath = new File(dumpClassFile.getParent()); + + // 创建类所在的包路径 + if (!classPath.mkdirs() + && !classPath.exists()) { + logger.warn("create dump classpath:{} failed.", classPath); + return; + } + + // 将类字节码写入文件 + try { + FileUtils.writeByteArrayToFile(dumpClassFile, data); + affect.getClassDumpFiles().add(dumpClassFile); + } catch (IOException e) { + logger.warn("dump class:{} to file {} failed.", className, dumpClassFile, e); + } + + } + + + /** + * 是否需要过滤的类 + * + * @param classes 类集合 + */ + private static void filter(Set> classes) { + final Iterator> it = classes.iterator(); + while (it.hasNext()) { + final Class clazz = it.next(); + if (null == clazz + || isSelf(clazz) + || isUnsafeClass(clazz) + || isUnsupportedClass(clazz)) { + it.remove(); + } + } + } + + /** + * 是否过滤Arthas加载的类 + */ + private static boolean isSelf(Class clazz) { + return null != clazz + && isEquals(clazz.getClassLoader(), Enhancer.class.getClassLoader()); + } + + /** + * 是否过滤unsafe类 + */ + private static boolean isUnsafeClass(Class clazz) { + return !GlobalOptions.isUnsafe + && clazz.getClassLoader() == null; + } + + /** + * 是否过滤目前暂不支持的类 + */ + private static boolean isUnsupportedClass(Class clazz) { + + return clazz.isArray() + || clazz.isInterface() + || clazz.isEnum() + || clazz.equals(Class.class) || clazz.equals(Integer.class) || clazz.equals(Method.class); + } + + /** + * 对象增强 + * + * @param inst inst + * @param adviceId 通知ID + * @param isTracing 可跟踪方法调用 + * @param classNameMatcher 类名匹配 + * @param methodNameMatcher 方法名匹配 + * @return 增强影响范围 + * @throws UnmodifiableClassException 增强失败 + */ + public static synchronized EnhancerAffect enhance( + final Instrumentation inst, + final int adviceId, + final boolean isTracing, + final boolean skipJDKTrace, + final Matcher classNameMatcher, + final Matcher methodNameMatcher) throws UnmodifiableClassException { + + final EnhancerAffect affect = new EnhancerAffect(); + + // 获取需要增强的类集合 + final Set> enhanceClassSet = GlobalOptions.isDisableSubClass + ? SearchUtils.searchClass(inst, classNameMatcher) + : SearchUtils.searchSubClass(inst, SearchUtils.searchClass(inst, classNameMatcher)); + + // 过滤掉无法被增强的类 + filter(enhanceClassSet); + + // 构建增强器 + final Enhancer enhancer = new Enhancer(adviceId, isTracing, skipJDKTrace, enhanceClassSet, methodNameMatcher, affect); + try { + inst.addTransformer(enhancer, true); + + // 批量增强 + if (GlobalOptions.isBatchReTransform) { + final int size = enhanceClassSet.size(); + final Class[] classArray = new Class[size]; + arraycopy(enhanceClassSet.toArray(), 0, classArray, 0, size); + if (classArray.length > 0) { + inst.retransformClasses(classArray); + logger.info("Success to batch transform classes: " + Arrays.toString(classArray)); + } + } else { + // for each 增强 + for (Class clazz : enhanceClassSet) { + try { + inst.retransformClasses(clazz); + logger.info("Success to transform class: " + clazz); + } catch (Throwable t) { + logger.warn("retransform {} failed.", clazz, t); + if (t instanceof UnmodifiableClassException) { + throw (UnmodifiableClassException) t; + } else if (t instanceof RuntimeException) { + throw (RuntimeException) t; + } else { + throw new RuntimeException(t); + } + } + } + } + } finally { + inst.removeTransformer(enhancer); + } + + return affect; + } + + + /** + * 重置指定的Class + * + * @param inst inst + * @param classNameMatcher 类名匹配 + * @return 增强影响范围 + * @throws UnmodifiableClassException + */ + public static synchronized EnhancerAffect reset( + final Instrumentation inst, + final Matcher classNameMatcher) throws UnmodifiableClassException { + + final EnhancerAffect affect = new EnhancerAffect(); + final Set> enhanceClassSet = new HashSet>(); + + for (Class classInCache : classBytesCache.keySet()) { + if (classNameMatcher.matching(classInCache.getName())) { + enhanceClassSet.add(classInCache); + } + } + + final ClassFileTransformer resetClassFileTransformer = new ClassFileTransformer() { + @Override + public byte[] transform( + ClassLoader loader, + String className, + Class classBeingRedefined, + ProtectionDomain protectionDomain, + byte[] classfileBuffer) throws IllegalClassFormatException { + return null; + } + }; + + try { + enhance(inst, resetClassFileTransformer, enhanceClassSet); + logger.info("Success to reset classes: " + enhanceClassSet); + } finally { + for (Class resetClass : enhanceClassSet) { + classBytesCache.remove(resetClass); + affect.cCnt(1); + } + } + + return affect; + } + + // 批量增强 + public static void enhance(Instrumentation inst, ClassFileTransformer transformer, Set> classes) + throws UnmodifiableClassException { + try { + inst.addTransformer(transformer, true); + int size = classes.size(); + Class[] classArray = new Class[size]; + arraycopy(classes.toArray(), 0, classArray, 0, size); + if (classArray.length > 0) { + inst.retransformClasses(classArray); + } + } finally { + inst.removeTransformer(transformer); + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/advisor/InvokeTraceable.java b/core/src/main/java/com/taobao/arthas/core/advisor/InvokeTraceable.java new file mode 100644 index 00000000..c7e1eca9 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/advisor/InvokeTraceable.java @@ -0,0 +1,51 @@ +package com.taobao.arthas.core.advisor; + +/** + * 方法调用跟踪
+ * 当一个方法内部调用另外一个方法时,会出发此跟踪方法 + * Created by vlinux on 15/5/27. + */ +public interface InvokeTraceable { + + /** + * 调用之前跟踪 + * + * @param tracingClassName 调用类名 + * @param tracingMethodName 调用方法名 + * @param tracingMethodDesc 调用方法描述 + * @throws Throwable 通知过程出错 + */ + void invokeBeforeTracing( + String tracingClassName, + String tracingMethodName, + String tracingMethodDesc) throws Throwable; + + /** + * 抛异常后跟踪 + * + * @param tracingClassName 调用类名 + * @param tracingMethodName 调用方法名 + * @param tracingMethodDesc 调用方法描述 + * @throws Throwable 通知过程出错 + */ + void invokeThrowTracing( + String tracingClassName, + String tracingMethodName, + String tracingMethodDesc) throws Throwable; + + + /** + * 调用之后跟踪 + * + * @param tracingClassName 调用类名 + * @param tracingMethodName 调用方法名 + * @param tracingMethodDesc 调用方法描述 + * @throws Throwable 通知过程出错 + */ + void invokeAfterTracing( + String tracingClassName, + String tracingMethodName, + String tracingMethodDesc) throws Throwable; + + +} diff --git a/core/src/main/java/com/taobao/arthas/core/advisor/ReflectAdviceListenerAdapter.java b/core/src/main/java/com/taobao/arthas/core/advisor/ReflectAdviceListenerAdapter.java new file mode 100644 index 00000000..cde1d75f --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/advisor/ReflectAdviceListenerAdapter.java @@ -0,0 +1,233 @@ +package com.taobao.arthas.core.advisor; + +import com.taobao.arthas.core.command.express.ExpressException; +import com.taobao.arthas.core.command.express.ExpressFactory; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.util.ArthasCheckUtils; +import com.taobao.arthas.core.util.Constants; +import com.taobao.arthas.core.util.StringUtils; +import org.objectweb.asm.Type; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +/** + * 反射通知适配器
+ * 通过反射拿到对应的Class/Method类,而不是原始的ClassName/MethodName + * 当然性能开销要比普通监听器高许多 + */ +public abstract class ReflectAdviceListenerAdapter implements AdviceListener { + + @Override + public void create() { + // default no-op + } + + @Override + public void destroy() { + // default no-op + } + + private ClassLoader toClassLoader(ClassLoader loader) { + return null != loader + ? loader + : AdviceListener.class.getClassLoader(); + } + + private Class toClass(ClassLoader loader, String className) throws ClassNotFoundException { + return Class.forName(StringUtils.normalizeClassName(className), true, toClassLoader(loader)); + } + + private ArthasMethod toMethod(ClassLoader loader, Class clazz, String methodName, String methodDesc) + throws ClassNotFoundException, NoSuchMethodException { + final org.objectweb.asm.Type asmType = org.objectweb.asm.Type.getMethodType(methodDesc); + + // to arg types + final Class[] argsClasses = new Class[asmType.getArgumentTypes().length]; + for (int index = 0; index < argsClasses.length; index++) { + // asm class descriptor to jvm class + final Class argumentClass; + final Type argumentAsmType = asmType.getArgumentTypes()[index]; + switch (argumentAsmType.getSort()) { + case Type.BOOLEAN: { + argumentClass = boolean.class; + break; + } + case Type.CHAR: { + argumentClass = char.class; + break; + } + case Type.BYTE: { + argumentClass = byte.class; + break; + } + case Type.SHORT: { + argumentClass = short.class; + break; + } + case Type.INT: { + argumentClass = int.class; + break; + } + case Type.FLOAT: { + argumentClass = float.class; + break; + } + case Type.LONG: { + argumentClass = long.class; + break; + } + case Type.DOUBLE: { + argumentClass = double.class; + break; + } + case Type.ARRAY: { + argumentClass = toClass(loader, argumentAsmType.getInternalName()); + break; + } + case Type.VOID: { + argumentClass = void.class; + break; + } + case Type.OBJECT: + case Type.METHOD: + default: { + argumentClass = toClass(loader, argumentAsmType.getClassName()); + break; + } + } + + argsClasses[index] = argumentClass; + } + + // to method or constructor + if (ArthasCheckUtils.isEquals(methodName, "")) { + return ArthasMethod.newInit(toConstructor(clazz, argsClasses)); + } else { + return ArthasMethod.newMethod(toMethod(clazz, methodName, argsClasses)); + } + } + + private Method toMethod(Class clazz, String methodName, Class[] argClasses) throws NoSuchMethodException { + return clazz.getDeclaredMethod(methodName, argClasses); + } + + private Constructor toConstructor(Class clazz, Class[] argClasses) throws NoSuchMethodException { + return clazz.getDeclaredConstructor(argClasses); + } + + + @Override + final public void before( + ClassLoader loader, String className, String methodName, String methodDesc, + Object target, Object[] args) throws Throwable { + final Class clazz = toClass(loader, className); + before(loader, clazz, toMethod(loader, clazz, methodName, methodDesc), target, args); + } + + @Override + final public void afterReturning( + ClassLoader loader, String className, String methodName, String methodDesc, + Object target, Object[] args, Object returnObject) throws Throwable { + final Class clazz = toClass(loader, className); + afterReturning(loader, clazz, toMethod(loader, clazz, methodName, methodDesc), target, args, returnObject); + } + + @Override + final public void afterThrowing( + ClassLoader loader, String className, String methodName, String methodDesc, + Object target, Object[] args, Throwable throwable) throws Throwable { + final Class clazz = toClass(loader, className); + afterThrowing(loader, clazz, toMethod(loader, clazz, methodName, methodDesc), target, args, throwable); + } + + + /** + * 前置通知 + * + * @param loader 类加载器 + * @param clazz 类 + * @param method 方法 + * @param target 目标类实例 + * 若目标为静态方法,则为null + * @param args 参数列表 + * @throws Throwable 通知过程出错 + */ + public abstract void before( + ClassLoader loader, Class clazz, ArthasMethod method, + Object target, Object[] args) throws Throwable; + + /** + * 返回通知 + * + * @param loader 类加载器 + * @param clazz 类 + * @param method 方法 + * @param target 目标类实例 + * 若目标为静态方法,则为null + * @param args 参数列表 + * @param returnObject 返回结果 + * 若为无返回值方法(void),则为null + * @throws Throwable 通知过程出错 + */ + public abstract void afterReturning( + ClassLoader loader, Class clazz, ArthasMethod method, + Object target, Object[] args, + Object returnObject) throws Throwable; + + /** + * 异常通知 + * + * @param loader 类加载器 + * @param clazz 类 + * @param method 方法 + * @param target 目标类实例 + * 若目标为静态方法,则为null + * @param args 参数列表 + * @param throwable 目标异常 + * @throws Throwable 通知过程出错 + */ + public abstract void afterThrowing( + ClassLoader loader, Class clazz, ArthasMethod method, + Object target, Object[] args, + Throwable throwable) throws Throwable; + + + /** + * 判断条件是否满足,满足的情况下需要输出结果 + * @param conditionExpress 条件表达式 + * @param advice 当前的advice对象 + * @param cost 本次执行的耗时 + * @return true 如果条件表达式满足 + */ + protected boolean isConditionMet(String conditionExpress, Advice advice, double cost) throws ExpressException { + return StringUtils.isEmpty(conditionExpress) || + ExpressFactory.newExpress(advice).bind(Constants.COST_VARIABLE, cost).is(conditionExpress); + } + + protected Object getExpressionResult(String express, Advice advice, double cost) throws ExpressException { + return ExpressFactory.newExpress(advice) + .bind(Constants.COST_VARIABLE, cost).get(express); + } + + /** + * 是否超过了上限,超过之后,停止输出 + * @param limit 命令执行上限 + * @param currentTimes 当前执行次数 + * @return true 如果超过或者达到了上限 + */ + protected boolean isLimitExceeded(int limit, int currentTimes) { + return currentTimes >= limit; + } + + /** + * 超过次数上限,则不在输出,命令终止 + * @param process the process to be aborted + * @param limit the limit to be printed + */ + protected void abortProcess(CommandProcess process, int limit) { + process.write("Command execution times exceed limit: " + limit + ", so command will exit. You can set it with -n option.\n"); + process.end(); + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/advisor/TracingAsmCodeLock.java b/core/src/main/java/com/taobao/arthas/core/advisor/TracingAsmCodeLock.java new file mode 100644 index 00000000..89b32cb8 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/advisor/TracingAsmCodeLock.java @@ -0,0 +1,22 @@ +package com.taobao.arthas.core.advisor; + +import org.objectweb.asm.commons.AdviceAdapter; + +/** + * 用于Tracing的代码锁 + * @author ralf0131 2016-12-28 16:46. + */ +public class TracingAsmCodeLock extends AsmCodeLock { + + public TracingAsmCodeLock(AdviceAdapter aa) { + super( + aa, + new int[]{ + ACONST_NULL, ICONST_0, ICONST_1, SWAP, SWAP, POP2, POP + }, + new int[]{ + ICONST_1, ACONST_NULL, ICONST_0, SWAP, SWAP, POP, POP2 + } + ); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/BuiltinCommandPack.java b/core/src/main/java/com/taobao/arthas/core/command/BuiltinCommandPack.java new file mode 100644 index 00000000..5119c5c4 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/BuiltinCommandPack.java @@ -0,0 +1,82 @@ +package com.taobao.arthas.core.command; + +import com.taobao.arthas.core.command.basic1000.ClsCommand; +import com.taobao.arthas.core.command.basic1000.HelpCommand; +import com.taobao.arthas.core.command.basic1000.KeymapCommand; +import com.taobao.arthas.core.command.basic1000.ResetCommand; +import com.taobao.arthas.core.command.basic1000.SessionCommand; +import com.taobao.arthas.core.command.basic1000.ShutdownCommand; +import com.taobao.arthas.core.command.basic1000.SystemPropertyCommand; +import com.taobao.arthas.core.command.basic1000.VersionCommand; +import com.taobao.arthas.core.command.hidden.JulyCommand; +import com.taobao.arthas.core.command.hidden.OptionsCommand; +import com.taobao.arthas.core.command.hidden.ThanksCommand; +import com.taobao.arthas.core.command.klass100.ClassLoaderCommand; +import com.taobao.arthas.core.command.klass100.DumpClassCommand; +import com.taobao.arthas.core.command.klass100.GetStaticCommand; +import com.taobao.arthas.core.command.klass100.JadCommand; +import com.taobao.arthas.core.command.klass100.RedefineCommand; +import com.taobao.arthas.core.command.klass100.SearchClassCommand; +import com.taobao.arthas.core.command.klass100.SearchMethodCommand; +import com.taobao.arthas.core.command.monitor200.DashboardCommand; +import com.taobao.arthas.core.command.monitor200.GroovyScriptCommand; +import com.taobao.arthas.core.command.monitor200.JvmCommand; +import com.taobao.arthas.core.command.monitor200.MonitorCommand; +import com.taobao.arthas.core.command.monitor200.StackCommand; +import com.taobao.arthas.core.command.monitor200.ThreadCommand; +import com.taobao.arthas.core.command.monitor200.TimeTunnelCommand; +import com.taobao.arthas.core.command.monitor200.TraceCommand; +import com.taobao.arthas.core.command.monitor200.WatchCommand; +import com.taobao.arthas.core.shell.command.Command; +import com.taobao.arthas.core.shell.command.CommandResolver; + +import java.util.ArrayList; +import java.util.List; + +/** + * TODO automatically discover the built-in commands. + * @author beiwei30 on 17/11/2016. + */ +public class BuiltinCommandPack implements CommandResolver { + + private static List commands = new ArrayList(); + + static { + initCommands(); + } + + @Override + public List commands() { + return commands; + } + + private static void initCommands() { + commands.add(Command.create(HelpCommand.class)); + commands.add(Command.create(KeymapCommand.class)); + commands.add(Command.create(SearchClassCommand.class)); + commands.add(Command.create(SearchMethodCommand.class)); + commands.add(Command.create(ClassLoaderCommand.class)); + commands.add(Command.create(JadCommand.class)); + commands.add(Command.create(GetStaticCommand.class)); + commands.add(Command.create(MonitorCommand.class)); + commands.add(Command.create(StackCommand.class)); + commands.add(Command.create(ThreadCommand.class)); + commands.add(Command.create(TraceCommand.class)); + commands.add(Command.create(WatchCommand.class)); + commands.add(Command.create(TimeTunnelCommand.class)); + commands.add(Command.create(JvmCommand.class)); + // commands.add(Command.create(GroovyScriptCommand.class)); + commands.add(Command.create(DashboardCommand.class)); + commands.add(Command.create(DumpClassCommand.class)); + commands.add(Command.create(JulyCommand.class)); + commands.add(Command.create(ThanksCommand.class)); + commands.add(Command.create(OptionsCommand.class)); + commands.add(Command.create(ClsCommand.class)); + commands.add(Command.create(ResetCommand.class)); + commands.add(Command.create(VersionCommand.class)); + commands.add(Command.create(ShutdownCommand.class)); + commands.add(Command.create(SessionCommand.class)); + commands.add(Command.create(SystemPropertyCommand.class)); + commands.add(Command.create(RedefineCommand.class)); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/Constants.java b/core/src/main/java/com/taobao/arthas/core/command/Constants.java new file mode 100644 index 00000000..617aaf6b --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/Constants.java @@ -0,0 +1,44 @@ +package com.taobao.arthas.core.command; + +/** + * @author ralf0131 2016-12-14 17:21. + */ +public interface Constants { + + /** + * TODO improve the description + */ + String EXPRESS_DESCRIPTION = " The express may be one of the following expression (evaluated dynamically):\n" + + " target : the object\n" + + " clazz : the object's class\n" + + " method : the constructor or method\n" + + " params[0..n] : the parameters of method\n" + + " returnObj : the returned object of method\n" + + " throwExp : the throw exception of method\n" + + " isReturn : the method ended by return\n" + + " isThrow : the method ended by throwing exception\n" + + " #cost : the execution time in ms of method invocation"; + + String EXAMPLE = "\nEXAMPLES:\n"; + + String WIKI = "\nWIKI:\n"; + + String WIKI_HOME = " middleware-container/arthas/wikis/"; + + String EXPRESS_EXAMPLES = "Examples:\n" + + " params[0]\n" + + " 'params[0]+params[1]'\n" + + " returnObj\n" + + " throwExp\n" + + " target\n" + + " clazz\n" + + " method\n"; + + String CONDITION_EXPRESS = "Conditional expression in ognl style, for example:\n" + + " TRUE : 1==1\n" + + " TRUE : true\n" + + " FALSE : false\n" + + " TRUE : 'params.length>=0'\n" + + " FALSE : 1==2\n"; + +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/ScriptSupportCommand.java b/core/src/main/java/com/taobao/arthas/core/command/ScriptSupportCommand.java new file mode 100644 index 00000000..c5937f6c --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/ScriptSupportCommand.java @@ -0,0 +1,118 @@ +package com.taobao.arthas.core.command; + +import com.taobao.arthas.core.advisor.Advice; + +/** + * 脚本支持命令 + * Created by vlinux on 15/6/1. + */ +public interface ScriptSupportCommand { + + /** + * 增强脚本监听器 + */ + interface ScriptListener { + + /** + * 脚本创建 + * + * @param output 输出器 + */ + void create(Output output); + + /** + * 脚本销毁 + * + * @param output 输出器 + */ + void destroy(Output output); + + /** + * 方法执行前 + * + * @param output 输出器 + * @param advice 通知点 + */ + void before(Output output, Advice advice); + + /** + * 方法正常返回 + * + * @param output 输出器 + * @param advice 通知点 + */ + void afterReturning(Output output, Advice advice); + + /** + * 方法异常返回 + * + * @param output 输出器 + * @param advice 通知点 + */ + void afterThrowing(Output output, Advice advice); + + } + + /** + * 脚本监听器适配器 + */ + class ScriptListenerAdapter implements ScriptListener { + + @Override + public void create(Output output) { + + } + + @Override + public void destroy(Output output) { + + } + + @Override + public void before(Output output, Advice advice) { + + } + + @Override + public void afterReturning(Output output, Advice advice) { + + } + + @Override + public void afterThrowing(Output output, Advice advice) { + + } + } + + + /** + * 输出器 + */ + interface Output { + + /** + * 输出字符串(不换行) + * + * @param string 待输出字符串 + * @return this + */ + Output print(String string); + + /** + * 输出字符串(换行) + * + * @param string 待输出字符串 + * @return this + */ + Output println(String string); + + /** + * 结束当前脚本 + * + * @return this + */ + Output finish(); + + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/basic1000/ClsCommand.java b/core/src/main/java/com/taobao/arthas/core/command/basic1000/ClsCommand.java new file mode 100644 index 00000000..f9ee183e --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/basic1000/ClsCommand.java @@ -0,0 +1,17 @@ +package com.taobao.arthas.core.command.basic1000; + +import com.taobao.arthas.core.shell.command.AnnotatedCommand; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Summary; +import com.taobao.text.util.RenderUtil; + +@Name("cls") +@Summary("Clear the screen") +public class ClsCommand extends AnnotatedCommand { + @Override + public void process(CommandProcess process) { + process.write(RenderUtil.cls()).write("\n"); + process.end(); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/basic1000/HelpCommand.java b/core/src/main/java/com/taobao/arthas/core/command/basic1000/HelpCommand.java new file mode 100644 index 00000000..8df0d692 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/basic1000/HelpCommand.java @@ -0,0 +1,88 @@ +package com.taobao.arthas.core.command.basic1000; + +import com.taobao.arthas.core.shell.command.AnnotatedCommand; +import com.taobao.arthas.core.shell.command.Command; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.shell.command.CommandResolver; +import com.taobao.arthas.core.util.usage.StyledUsageFormatter; +import com.taobao.middleware.cli.CLI; +import com.taobao.middleware.cli.annotations.Argument; +import com.taobao.middleware.cli.annotations.Description; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Summary; +import com.taobao.text.Color; +import com.taobao.text.Decoration; +import com.taobao.text.Style; +import com.taobao.text.ui.Element; +import com.taobao.text.ui.LabelElement; +import com.taobao.text.ui.TableElement; +import com.taobao.text.util.RenderUtil; + +import java.util.ArrayList; +import java.util.List; + +import static com.taobao.text.ui.Element.label; +import static com.taobao.text.ui.Element.row; + +/** + * @author vlinux on 14/10/26. + */ +@Name("help") +@Summary("Display Arthas Help") +@Description("Examples:\n" + " help\n" + " help sc\n" + " help sm\n" + " help watch") +public class HelpCommand extends AnnotatedCommand { + + private String cmd; + + @Argument(index = 0, argName = "cmd", required = false) + @Description("command name") + public void setCmd(String cmd) { + this.cmd = cmd; + } + + @Override + public void process(CommandProcess process) { + List commandResolvers = process.session().getCommandResolvers(); + List commands = new ArrayList(); + for (CommandResolver commandResolver : commandResolvers) { + commands.addAll(commandResolver.commands()); + } + + Command targetCmd = findCommand(commands); + String message; + if (targetCmd == null) { + message = RenderUtil.render(mainHelp(commands), process.width()); + } else { + message = commandHelp(targetCmd, process.width()); + } + process.write(message); + process.end(); + } + + private static String commandHelp(Command command, int width) { + return StyledUsageFormatter.styledUsage(command.cli(), width); + } + + private static Element mainHelp(List commands) { + TableElement table = new TableElement().leftCellPadding(1).rightCellPadding(1); + table.row(new LabelElement("NAME").style(Style.style(Decoration.bold)), new LabelElement("DESCRIPTION")); + for (Command command : commands) { + CLI cli = command.cli(); + // com.taobao.arthas.core.shell.impl.BuiltinCommandResolver doesn't have CLI instance + if (cli == null || cli.isHidden()) { + continue; + } + table.add(row().add(label(cli.getName()).style(Style.style(Color.green))).add(label(cli.getSummary()))); + } + return table; + } + + private Command findCommand(List commands) { + for (Command command : commands) { + if (command.name().equals(cmd)) { + return command; + } + } + return null; + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/basic1000/KeymapCommand.java b/core/src/main/java/com/taobao/arthas/core/command/basic1000/KeymapCommand.java new file mode 100644 index 00000000..5083a873 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/basic1000/KeymapCommand.java @@ -0,0 +1,40 @@ +package com.taobao.arthas.core.command.basic1000; + +import com.taobao.arthas.core.shell.command.AnnotatedCommand; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.shell.term.impl.Helper; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Summary; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; + +/** + * A command to display all the keymap for the specified connection. + * @author ralf0131 2016-12-15 17:27. + */ +@Name("keymap") +@Summary("Display all the available keymap for the specified connection.") +public class KeymapCommand extends AnnotatedCommand { + + @Override + public void process(CommandProcess process) { + InputStream inputrc = Helper.loadInputRcFile(); + BufferedReader br = new BufferedReader(new InputStreamReader(inputrc)); + StringBuilder sb = new StringBuilder(); + String line; + try { + while ((line = br.readLine()) != null) { + if (line.startsWith("#") || "".equals(line.trim())) { + continue; + } + sb.append(line + "\n"); + } + } catch (Exception e) { + sb.append(e.getMessage()); + } + process.write(sb.toString()); + process.end(); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/basic1000/ResetCommand.java b/core/src/main/java/com/taobao/arthas/core/command/basic1000/ResetCommand.java new file mode 100644 index 00000000..9080e7a2 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/basic1000/ResetCommand.java @@ -0,0 +1,60 @@ +package com.taobao.arthas.core.command.basic1000; + +import com.taobao.arthas.core.advisor.Enhancer; +import com.taobao.arthas.core.command.Constants; +import com.taobao.arthas.core.shell.command.AnnotatedCommand; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.util.matcher.Matcher; +import com.taobao.arthas.core.util.SearchUtils; +import com.taobao.arthas.core.util.affect.EnhancerAffect; +import com.taobao.middleware.cli.annotations.Argument; +import com.taobao.middleware.cli.annotations.Description; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Option; +import com.taobao.middleware.cli.annotations.Summary; + +import java.lang.instrument.Instrumentation; +import java.lang.instrument.UnmodifiableClassException; + +/** + * 恢复所有增强类
+ * + * @author vlinux on 15/5/29. + */ +@Name("reset") +@Summary("Reset all the enhanced classes") +@Description(Constants.EXAMPLE + + " reset\n" + + " reset *List\n" + + " reset -E .*List\n") +public class ResetCommand extends AnnotatedCommand { + private String classPattern; + private boolean isRegEx = false; + + @Argument(index = 0, argName = "class-pattern", required = false) + @Description("Path and classname of Pattern Matching") + public void setClassPattern(String classPattern) { + this.classPattern = classPattern; + } + + @Option(shortName = "E", longName = "regex", flag = true) + @Description("Enable regular expression to match (wildcard matching by default)") + public void setRegEx(boolean regEx) { + isRegEx = regEx; + } + + @Override + public void process(CommandProcess process) { + Instrumentation inst = process.session().getInstrumentation(); + Matcher matcher = SearchUtils.classNameMatcher(classPattern, isRegEx); + EnhancerAffect enhancerAffect = null; + try { + enhancerAffect = Enhancer.reset(inst, matcher); + process.write(enhancerAffect.toString()).write("\n"); + } catch (UnmodifiableClassException e) { + // ignore + } finally { + process.end(); + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/basic1000/SessionCommand.java b/core/src/main/java/com/taobao/arthas/core/command/basic1000/SessionCommand.java new file mode 100644 index 00000000..8ed0612b --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/basic1000/SessionCommand.java @@ -0,0 +1,37 @@ +package com.taobao.arthas.core.command.basic1000; + +import com.taobao.arthas.core.shell.command.AnnotatedCommand; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.shell.session.Session; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Summary; +import com.taobao.text.Decoration; +import com.taobao.text.ui.Element; +import com.taobao.text.ui.TableElement; +import com.taobao.text.util.RenderUtil; + +import static com.taobao.text.ui.Element.label; + +/** + * 查看会话状态命令 + * + * @author vlinux on 15/5/3. + */ +@Name("session") +@Summary("Display current session information") +public class SessionCommand extends AnnotatedCommand { + @Override + public void process(CommandProcess process) { + process.write(RenderUtil.render(sessionTable(process.session()), process.width())).end(); + } + + /* + * 会话详情 + */ + private Element sessionTable(Session session) { + TableElement table = new TableElement().leftCellPadding(1).rightCellPadding(1); + table.row(true, label("Name").style(Decoration.bold.bold()), label("Value").style(Decoration.bold.bold())); + table.row("JAVA_PID", "" + session.getPid()).row("SESSION_ID", "" + session.getSessionId()); + return table; + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/basic1000/ShutdownCommand.java b/core/src/main/java/com/taobao/arthas/core/command/basic1000/ShutdownCommand.java new file mode 100644 index 00000000..96df3992 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/basic1000/ShutdownCommand.java @@ -0,0 +1,39 @@ +package com.taobao.arthas.core.command.basic1000; + +import com.taobao.arthas.core.advisor.Enhancer; +import com.taobao.arthas.core.shell.ShellServer; +import com.taobao.arthas.core.shell.command.AnnotatedCommand; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.util.affect.EnhancerAffect; +import com.taobao.arthas.core.util.matcher.WildcardMatcher; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Summary; + +import java.lang.instrument.Instrumentation; +import java.lang.instrument.UnmodifiableClassException; + +/** + * 关闭命令 + * + * @author vlinux on 14/10/23. + */ +@Name("shutdown") +@Summary("Shut down Arthas server and exit the console") +public class ShutdownCommand extends AnnotatedCommand { + @Override + public void process(CommandProcess process) { + try { + // 退出之前需要重置所有的增强类 + Instrumentation inst = process.session().getInstrumentation(); + EnhancerAffect enhancerAffect = Enhancer.reset(inst, new WildcardMatcher("*")); + process.write(enhancerAffect.toString()).write("\n"); + process.write("Arthas Server is going to shut down...\n"); + } catch (UnmodifiableClassException e) { + // ignore + } finally { + process.end(); + ShellServer server = process.session().getServer(); + server.close(); + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/basic1000/SystemPropertyCommand.java b/core/src/main/java/com/taobao/arthas/core/command/basic1000/SystemPropertyCommand.java new file mode 100644 index 00000000..cd8e10d6 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/basic1000/SystemPropertyCommand.java @@ -0,0 +1,94 @@ +package com.taobao.arthas.core.command.basic1000; + +import com.taobao.arthas.core.command.Constants; +import com.taobao.arthas.core.shell.cli.Completion; +import com.taobao.arthas.core.shell.cli.CompletionUtils; +import com.taobao.arthas.core.shell.command.AnnotatedCommand; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.util.StringUtils; +import com.taobao.middleware.cli.annotations.Argument; +import com.taobao.middleware.cli.annotations.Description; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Summary; +import com.taobao.text.Decoration; +import com.taobao.text.ui.TableElement; +import com.taobao.text.util.RenderUtil; + +import java.util.Properties; + +import static com.taobao.text.ui.Element.label; + +/** + * A command to display all the keymap for the specified connection. + * @author ralf0131 2017-01-09 14:03. + */ +@Name("sysprop") +@Summary("Display, and change the system properties.") +@Description(Constants.EXAMPLE + "sysprop\n"+ "sysprop file.encoding\n" + "sysprop production.mode true\n" + + Constants.WIKI + Constants.WIKI_HOME + "cmds/sysprop") +public class SystemPropertyCommand extends AnnotatedCommand { + + private String propertyName; + private String propertyValue; + + @Argument(index = 0, argName = "property-name", required = false) + @Description("property name") + public void setOptionName(String propertyName) { + this.propertyName = propertyName; + } + + @Argument(index = 1, argName = "property-value", required = false) + @Description("property value") + public void setOptionValue(String propertyValue) { + this.propertyValue = propertyValue; + } + + @Override + public void process(CommandProcess process) { + try { + if (StringUtils.isBlank(propertyName) && StringUtils.isBlank(propertyValue)) { + // show all system properties + process.write(renderSystemProperties(System.getProperties(), process.width())); + } else if (StringUtils.isBlank(propertyValue)) { + // view the specified system property + String value = System.getProperty(propertyName); + if (value == null) { + process.write("In order to change the system properties, you must specify the property value.\n"); + } else { + process.write(propertyName + "=" + value + "\n"); + } + } else { + // change system property + System.setProperty(propertyName, propertyValue); + process.write("Successfully changed the system property.\n"); + process.write(propertyName + "=" + System.getProperty(propertyName) + "\n"); + } + } catch (Throwable t) { + process.write("Error during setting system property: " + t.getMessage() + "\n"); + } finally { + process.end(); + } + } + + /** + * First, try to complete with the sysprop command scope. + * If completion is failed, delegates to super class. + * @param completion the completion object + */ + @Override + public void complete(Completion completion) { + CompletionUtils.complete(completion, System.getProperties().stringPropertyNames()); + } + + private String renderSystemProperties(Properties properties, int width) { + TableElement table = new TableElement(1, 4).leftCellPadding(1).rightCellPadding(1); + table.row(true, label("KEY").style(Decoration.bold.bold()), + label("VALUE").style(Decoration.bold.bold())); + + for (String name: properties.stringPropertyNames()) { + table.row(name, properties.getProperty(name)); + } + + return RenderUtil.render(table, width); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/basic1000/VersionCommand.java b/core/src/main/java/com/taobao/arthas/core/command/basic1000/VersionCommand.java new file mode 100644 index 00000000..04b60773 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/basic1000/VersionCommand.java @@ -0,0 +1,22 @@ +package com.taobao.arthas.core.command.basic1000; + + +import com.taobao.arthas.core.shell.command.AnnotatedCommand; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.util.ArthasBanner; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Summary; + +/** + * 输出版本 + * + * @author vlinux + */ +@Name("version") +@Summary("Display Arthas version") +public class VersionCommand extends AnnotatedCommand { + @Override + public void process(CommandProcess process) { + process.write(ArthasBanner.version()).write("\n").end(); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/express/CustomClassResolver.java b/core/src/main/java/com/taobao/arthas/core/command/express/CustomClassResolver.java new file mode 100644 index 00000000..bf4387cb --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/express/CustomClassResolver.java @@ -0,0 +1,44 @@ +package com.taobao.arthas.core.command.express; + +import ognl.ClassResolver; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author diecui1202 on 2017/9/29. + */ +public class CustomClassResolver implements ClassResolver { + + public static final CustomClassResolver customClassResolver = new CustomClassResolver(); + + private static final ThreadLocal classLoader = new ThreadLocal(); + + private Map classes = new HashMap(101); + + private CustomClassResolver() { + + } + + public Class classForName(String className, Map context) throws ClassNotFoundException { + Class result = null; + + if ((result = (Class) classes.get(className)) == null) { + try { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + if (classLoader != null) { + result = classLoader.loadClass(className); + } else { + result = Class.forName(className); + } + } catch (ClassNotFoundException ex) { + if (className.indexOf('.') == -1) { + result = Class.forName("java.lang." + className); + classes.put("java.lang." + className, result); + } + } + classes.put(className, result); + } + return result; + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/express/Express.java b/core/src/main/java/com/taobao/arthas/core/command/express/Express.java new file mode 100644 index 00000000..0ad0a786 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/express/Express.java @@ -0,0 +1,52 @@ +package com.taobao.arthas.core.command.express; + +/** + * 表达式 + * Created by vlinux on 15/5/20. + */ +public interface Express { + + /** + * 根据表达式获取值 + * + * @param express 表达式 + * @return 表达式运算后的值 + * @throws ExpressException 表达式运算出错 + */ + Object get(String express) throws ExpressException; + + /** + * 根据表达式判断是与否 + * + * @param express 表达式 + * @return 表达式运算后的布尔值 + * @throws ExpressException 表达式运算出错 + */ + boolean is(String express) throws ExpressException; + + /** + * 绑定对象 + * + * @param object 待绑定对象 + * @return this + */ + Express bind(Object object); + + /** + * 绑定变量 + * + * @param name 变量名 + * @param value 变量值 + * @return this + */ + Express bind(String name, Object value); + + /** + * 重置整个表达式 + * + * @return this + */ + Express reset(); + + +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/express/ExpressException.java b/core/src/main/java/com/taobao/arthas/core/command/express/ExpressException.java new file mode 100644 index 00000000..7c3d1e92 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/express/ExpressException.java @@ -0,0 +1,30 @@ +package com.taobao.arthas.core.command.express; + +/** + * 表达式异常 + * Created by vlinux on 15/5/20. + */ +public class ExpressException extends Exception { + + private final String express; + + /** + * 表达式异常 + * + * @param express 原始表达式 + * @param cause 异常原因 + */ + public ExpressException(String express, Throwable cause) { + super(cause); + this.express = express; + } + + /** + * 获取表达式 + * + * @return 返回出问题的表达式 + */ + public String getExpress() { + return express; + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/express/ExpressFactory.java b/core/src/main/java/com/taobao/arthas/core/command/express/ExpressFactory.java new file mode 100644 index 00000000..958b5e57 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/express/ExpressFactory.java @@ -0,0 +1,26 @@ +package com.taobao.arthas.core.command.express; + +/** + * 表达式工厂类 + * @author ralf0131 2017-01-04 14:40. + */ +public class ExpressFactory { + + private static final ThreadLocal expressRef = new ThreadLocal() { + @Override + protected Express initialValue() { + return new OgnlExpress(); + } + }; + + /** + * 构造表达式执行类 + * + * @param object 执行对象 + * @return 返回表达式实现 + */ + public static Express newExpress(Object object) { + return expressRef.get().reset().bind(object); + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/taobao/arthas/core/command/express/OgnlExpress.java b/core/src/main/java/com/taobao/arthas/core/command/express/OgnlExpress.java new file mode 100644 index 00000000..9ff7042e --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/express/OgnlExpress.java @@ -0,0 +1,59 @@ +package com.taobao.arthas.core.command.express; + +import com.taobao.arthas.core.util.LogUtil; +import com.taobao.middleware.logger.Logger; +import ognl.DefaultMemberAccess; +import ognl.Ognl; +import ognl.OgnlContext; + +/** + * @author ralf0131 2017-01-04 14:41. + */ +public class OgnlExpress implements Express { + + Logger logger = LogUtil.getArthasLogger(); + + private Object bindObject; + private final OgnlContext context; + + public OgnlExpress() { + context = new OgnlContext(); + context.setClassResolver(CustomClassResolver.customClassResolver); + } + + @Override + public Object get(String express) throws ExpressException { + try { + context.setMemberAccess(new DefaultMemberAccess(true)); + return Ognl.getValue(express, context, bindObject); + } catch (Exception e) { + logger.error(null, "Error during evaluating the expression:", e); + throw new ExpressException(express, e); + } + } + + @Override + public boolean is(String express) throws ExpressException { + final Object ret = get(express); + return null != ret && ret instanceof Boolean && (Boolean) ret; + } + + @Override + public Express bind(Object object) { + this.bindObject = object; + return this; + } + + @Override + public Express bind(String name, Object value) { + context.put(name, value); + return this; + } + + @Override + public Express reset() { + context.clear(); + context.setClassResolver(CustomClassResolver.customClassResolver); + return this; + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/hidden/JulyCommand.java b/core/src/main/java/com/taobao/arthas/core/command/hidden/JulyCommand.java new file mode 100644 index 00000000..f67afbbd --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/hidden/JulyCommand.java @@ -0,0 +1,105 @@ +package com.taobao.arthas.core.command.hidden; + +import com.taobao.arthas.core.shell.command.AnnotatedCommand; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.middleware.cli.annotations.Hidden; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Summary; + +/** + * @author vlinux on 02/11/2016. + */ +@Name("july") +@Summary("don't ask why") +@Hidden +public class JulyCommand extends AnnotatedCommand { + @Override + public void process(CommandProcess process) { + process.write(new String($$())).write("\n").end(); + } + + private static byte[] $$() { + return new byte[]{ + 0x49, 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x6d, 0x61, 0x6b, 0x65, 0x20, 0x74, 0x68, 0x65, + 0x20, 0x73, 0x61, 0x6d, 0x65, 0x20, 0x6d, 0x69, 0x73, 0x74, 0x61, 0x6b, 0x65, 0x73, 0x20, 0x74, 0x68, 0x61, 0x74, + 0x20, 0x79, 0x6f, 0x75, 0x20, 0x64, 0x69, 0x64, 0x0a, 0x49, 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x6e, 0x6f, 0x74, + 0x20, 0x6c, 0x65, 0x74, 0x20, 0x6d, 0x79, 0x73, 0x65, 0x6c, 0x66, 0x0a, 0x43, 0x61, 0x75, 0x73, 0x65, 0x20, 0x6d, + 0x79, 0x20, 0x68, 0x65, 0x61, 0x72, 0x74, 0x20, 0x73, 0x6f, 0x20, 0x6d, 0x75, 0x63, 0x68, 0x20, 0x6d, 0x69, 0x73, + 0x65, 0x72, 0x79, 0x0a, 0x49, 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x72, 0x65, 0x61, + 0x6b, 0x20, 0x74, 0x68, 0x65, 0x20, 0x77, 0x61, 0x79, 0x20, 0x79, 0x6f, 0x75, 0x20, 0x64, 0x69, 0x64, 0x0a, 0x59, + 0x6f, 0x75, 0x20, 0x66, 0x65, 0x6c, 0x6c, 0x20, 0x73, 0x6f, 0x20, 0x68, 0x61, 0x72, 0x64, 0x0a, 0x0a, 0x49, 0x20, + 0x76, 0x65, 0x20, 0x6c, 0x65, 0x61, 0x72, 0x6e, 0x65, 0x64, 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x61, 0x72, 0x64, + 0x20, 0x77, 0x61, 0x79, 0x0a, 0x54, 0x6f, 0x20, 0x6e, 0x65, 0x76, 0x65, 0x72, 0x20, 0x6c, 0x65, 0x74, 0x20, 0x69, + 0x74, 0x20, 0x67, 0x65, 0x74, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, 0x66, 0x61, 0x72, 0x0a, 0x0a, 0x42, 0x65, 0x63, + 0x61, 0x75, 0x73, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x79, 0x6f, 0x75, 0x0a, 0x49, 0x20, 0x6e, 0x65, 0x76, 0x65, 0x72, + 0x20, 0x73, 0x74, 0x72, 0x61, 0x79, 0x20, 0x74, 0x6f, 0x6f, 0x20, 0x66, 0x61, 0x72, 0x20, 0x66, 0x72, 0x6f, 0x6d, + 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x69, 0x64, 0x65, 0x77, 0x61, 0x6c, 0x6b, 0x0a, 0x42, 0x65, 0x63, 0x61, 0x75, + 0x73, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x79, 0x6f, 0x75, 0x0a, 0x49, 0x20, 0x6c, 0x65, 0x61, 0x72, 0x6e, 0x65, 0x64, + 0x20, 0x74, 0x6f, 0x20, 0x70, 0x6c, 0x61, 0x79, 0x20, 0x6f, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x61, 0x66, + 0x65, 0x20, 0x73, 0x69, 0x64, 0x65, 0x20, 0x73, 0x6f, 0x20, 0x49, 0x20, 0x64, 0x6f, 0x6e, 0x20, 0x74, 0x20, 0x67, + 0x65, 0x74, 0x20, 0x68, 0x75, 0x72, 0x74, 0x0a, 0x42, 0x65, 0x63, 0x61, 0x75, 0x73, 0x65, 0x20, 0x6f, 0x66, 0x20, + 0x79, 0x6f, 0x75, 0x0a, 0x49, 0x20, 0x66, 0x69, 0x6e, 0x64, 0x20, 0x69, 0x74, 0x20, 0x68, 0x61, 0x72, 0x64, 0x20, + 0x74, 0x6f, 0x20, 0x74, 0x72, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x6f, 0x6e, 0x6c, 0x79, 0x20, 0x6d, + 0x65, 0x2c, 0x20, 0x62, 0x75, 0x74, 0x20, 0x65, 0x76, 0x65, 0x72, 0x79, 0x6f, 0x6e, 0x65, 0x20, 0x61, 0x72, 0x6f, + 0x75, 0x6e, 0x64, 0x20, 0x6d, 0x65, 0x0a, 0x42, 0x65, 0x63, 0x61, 0x75, 0x73, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x79, + 0x6f, 0x75, 0x0a, 0x49, 0x20, 0x61, 0x6d, 0x20, 0x61, 0x66, 0x72, 0x61, 0x69, 0x64, 0x0a, 0x0a, 0x49, 0x20, 0x6c, + 0x6f, 0x73, 0x65, 0x20, 0x6d, 0x79, 0x20, 0x77, 0x61, 0x79, 0x0a, 0x41, 0x6e, 0x64, 0x20, 0x69, 0x74, 0x20, 0x73, + 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x74, 0x6f, 0x6f, 0x20, 0x6c, 0x6f, 0x6e, 0x67, 0x20, 0x62, 0x65, 0x66, 0x6f, 0x72, + 0x65, 0x20, 0x79, 0x6f, 0x75, 0x20, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x20, 0x69, 0x74, 0x20, 0x6f, 0x75, 0x74, 0x0a, + 0x49, 0x20, 0x63, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x20, 0x63, 0x72, 0x79, 0x0a, 0x42, 0x65, 0x63, 0x61, 0x75, 0x73, + 0x65, 0x20, 0x79, 0x6f, 0x75, 0x20, 0x6b, 0x6e, 0x6f, 0x77, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, 0x73, 0x20, 0x77, + 0x65, 0x61, 0x6b, 0x6e, 0x65, 0x73, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x79, 0x6f, 0x75, 0x72, 0x20, 0x65, 0x79, 0x65, + 0x73, 0x0a, 0x49, 0x20, 0x6d, 0x20, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x61, 0x6b, + 0x65, 0x0a, 0x41, 0x20, 0x73, 0x6d, 0x69, 0x6c, 0x65, 0x2c, 0x20, 0x61, 0x20, 0x6c, 0x61, 0x75, 0x67, 0x68, 0x20, + 0x65, 0x76, 0x65, 0x72, 0x79, 0x64, 0x61, 0x79, 0x20, 0x6f, 0x66, 0x20, 0x6d, 0x79, 0x20, 0x6c, 0x69, 0x66, 0x65, + 0x0a, 0x4d, 0x79, 0x20, 0x68, 0x65, 0x61, 0x72, 0x74, 0x20, 0x63, 0x61, 0x6e, 0x27, 0x74, 0x20, 0x70, 0x6f, 0x73, + 0x73, 0x69, 0x62, 0x6c, 0x79, 0x20, 0x62, 0x72, 0x65, 0x61, 0x6b, 0x0a, 0x57, 0x68, 0x65, 0x6e, 0x20, 0x69, 0x74, + 0x20, 0x77, 0x61, 0x73, 0x6e, 0x27, 0x74, 0x20, 0x65, 0x76, 0x65, 0x6e, 0x20, 0x77, 0x68, 0x6f, 0x6c, 0x65, 0x20, + 0x74, 0x6f, 0x20, 0x73, 0x74, 0x61, 0x72, 0x74, 0x20, 0x77, 0x69, 0x74, 0x68, 0x0a, 0x0a, 0x42, 0x65, 0x63, 0x61, + 0x75, 0x73, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x79, 0x6f, 0x75, 0x0a, 0x49, 0x20, 0x6e, 0x65, 0x76, 0x65, 0x72, 0x20, + 0x73, 0x74, 0x72, 0x61, 0x79, 0x20, 0x74, 0x6f, 0x6f, 0x20, 0x66, 0x61, 0x72, 0x20, 0x66, 0x72, 0x6f, 0x6d, 0x20, + 0x74, 0x68, 0x65, 0x20, 0x73, 0x69, 0x64, 0x65, 0x77, 0x61, 0x6c, 0x6b, 0x0a, 0x42, 0x65, 0x63, 0x61, 0x75, 0x73, + 0x65, 0x20, 0x6f, 0x66, 0x20, 0x79, 0x6f, 0x75, 0x0a, 0x49, 0x20, 0x6c, 0x65, 0x61, 0x72, 0x6e, 0x65, 0x64, 0x20, + 0x74, 0x6f, 0x20, 0x70, 0x6c, 0x61, 0x79, 0x20, 0x6f, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x61, 0x66, 0x65, + 0x20, 0x73, 0x69, 0x64, 0x65, 0x20, 0x73, 0x6f, 0x20, 0x49, 0x20, 0x64, 0x6f, 0x6e, 0x20, 0x74, 0x20, 0x67, 0x65, + 0x74, 0x20, 0x68, 0x75, 0x72, 0x74, 0x0a, 0x42, 0x65, 0x63, 0x61, 0x75, 0x73, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x79, + 0x6f, 0x75, 0x0a, 0x49, 0x20, 0x66, 0x69, 0x6e, 0x64, 0x20, 0x69, 0x74, 0x20, 0x68, 0x61, 0x72, 0x64, 0x20, 0x74, + 0x6f, 0x20, 0x74, 0x72, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x6f, 0x6e, 0x6c, 0x79, 0x20, 0x6d, 0x65, + 0x2c, 0x20, 0x62, 0x75, 0x74, 0x20, 0x65, 0x76, 0x65, 0x72, 0x79, 0x6f, 0x6e, 0x65, 0x20, 0x61, 0x72, 0x6f, 0x75, + 0x6e, 0x64, 0x20, 0x6d, 0x65, 0x0a, 0x42, 0x65, 0x63, 0x61, 0x75, 0x73, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x79, 0x6f, + 0x75, 0x0a, 0x49, 0x20, 0x61, 0x6d, 0x20, 0x61, 0x66, 0x72, 0x61, 0x69, 0x64, 0x0a, 0x0a, 0x49, 0x20, 0x77, 0x61, + 0x74, 0x63, 0x68, 0x65, 0x64, 0x20, 0x79, 0x6f, 0x75, 0x20, 0x64, 0x69, 0x65, 0x0a, 0x49, 0x20, 0x68, 0x65, 0x61, + 0x72, 0x64, 0x20, 0x79, 0x6f, 0x75, 0x20, 0x63, 0x72, 0x79, 0x20, 0x65, 0x76, 0x65, 0x72, 0x79, 0x20, 0x6e, 0x69, + 0x67, 0x68, 0x74, 0x20, 0x69, 0x6e, 0x20, 0x79, 0x6f, 0x75, 0x72, 0x20, 0x73, 0x6c, 0x65, 0x65, 0x70, 0x0a, 0x49, + 0x20, 0x77, 0x61, 0x73, 0x20, 0x73, 0x6f, 0x20, 0x79, 0x6f, 0x75, 0x6e, 0x67, 0x0a, 0x59, 0x6f, 0x75, 0x20, 0x73, + 0x68, 0x6f, 0x75, 0x6c, 0x64, 0x20, 0x68, 0x61, 0x76, 0x65, 0x20, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x20, 0x62, 0x65, + 0x74, 0x74, 0x65, 0x72, 0x20, 0x74, 0x68, 0x61, 0x6e, 0x20, 0x74, 0x6f, 0x20, 0x6c, 0x65, 0x61, 0x6e, 0x20, 0x6f, + 0x6e, 0x20, 0x6d, 0x65, 0x0a, 0x59, 0x6f, 0x75, 0x20, 0x6e, 0x65, 0x76, 0x65, 0x72, 0x20, 0x74, 0x68, 0x6f, 0x75, + 0x67, 0x68, 0x74, 0x20, 0x6f, 0x66, 0x20, 0x61, 0x6e, 0x79, 0x6f, 0x6e, 0x65, 0x20, 0x65, 0x6c, 0x73, 0x65, 0x0a, + 0x59, 0x6f, 0x75, 0x20, 0x6a, 0x75, 0x73, 0x74, 0x20, 0x73, 0x61, 0x77, 0x20, 0x79, 0x6f, 0x75, 0x72, 0x20, 0x70, + 0x61, 0x69, 0x6e, 0x0a, 0x41, 0x6e, 0x64, 0x20, 0x6e, 0x6f, 0x77, 0x20, 0x49, 0x20, 0x63, 0x72, 0x79, 0x20, 0x69, + 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, + 0x20, 0x6e, 0x69, 0x67, 0x68, 0x74, 0x0a, 0x46, 0x6f, 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x61, 0x6d, 0x65, + 0x20, 0x64, 0x61, 0x6d, 0x6e, 0x20, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x0a, 0x0a, 0x42, 0x65, 0x63, 0x61, 0x75, 0x73, + 0x65, 0x20, 0x6f, 0x66, 0x20, 0x79, 0x6f, 0x75, 0x0a, 0x49, 0x20, 0x6e, 0x65, 0x76, 0x65, 0x72, 0x20, 0x73, 0x74, + 0x72, 0x61, 0x79, 0x20, 0x74, 0x6f, 0x6f, 0x20, 0x66, 0x61, 0x72, 0x20, 0x66, 0x72, 0x6f, 0x6d, 0x20, 0x74, 0x68, + 0x65, 0x20, 0x73, 0x69, 0x64, 0x65, 0x77, 0x61, 0x6c, 0x6b, 0x0a, 0x42, 0x65, 0x63, 0x61, 0x75, 0x73, 0x65, 0x20, + 0x6f, 0x66, 0x20, 0x79, 0x6f, 0x75, 0x0a, 0x49, 0x20, 0x6c, 0x65, 0x61, 0x72, 0x6e, 0x65, 0x64, 0x20, 0x74, 0x6f, + 0x20, 0x70, 0x6c, 0x61, 0x79, 0x20, 0x6f, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x61, 0x66, 0x65, 0x20, 0x73, + 0x69, 0x64, 0x65, 0x20, 0x73, 0x6f, 0x20, 0x49, 0x20, 0x64, 0x6f, 0x6e, 0x20, 0x74, 0x20, 0x67, 0x65, 0x74, 0x20, + 0x68, 0x75, 0x72, 0x74, 0x0a, 0x42, 0x65, 0x63, 0x61, 0x75, 0x73, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x79, 0x6f, 0x75, + 0x0a, 0x49, 0x20, 0x74, 0x72, 0x79, 0x20, 0x6d, 0x79, 0x20, 0x68, 0x61, 0x72, 0x64, 0x65, 0x73, 0x74, 0x20, 0x6a, + 0x75, 0x73, 0x74, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x20, 0x65, 0x76, 0x65, 0x72, 0x79, + 0x74, 0x68, 0x69, 0x6e, 0x67, 0x0a, 0x42, 0x65, 0x63, 0x61, 0x75, 0x73, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x79, 0x6f, + 0x75, 0x0a, 0x49, 0x20, 0x64, 0x6f, 0x6e, 0x20, 0x74, 0x20, 0x6b, 0x6e, 0x6f, 0x77, 0x20, 0x68, 0x6f, 0x77, 0x20, + 0x74, 0x6f, 0x20, 0x6c, 0x65, 0x74, 0x20, 0x61, 0x6e, 0x79, 0x6f, 0x6e, 0x65, 0x20, 0x65, 0x6c, 0x73, 0x65, 0x20, + 0x69, 0x6e, 0x0a, 0x42, 0x65, 0x63, 0x61, 0x75, 0x73, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x79, 0x6f, 0x75, 0x0a, 0x49, + 0x20, 0x6d, 0x20, 0x61, 0x73, 0x68, 0x61, 0x6d, 0x65, 0x64, 0x20, 0x6f, 0x66, 0x20, 0x6d, 0x79, 0x20, 0x6c, 0x69, + 0x66, 0x65, 0x20, 0x62, 0x65, 0x63, 0x61, 0x75, 0x73, 0x65, 0x20, 0x69, 0x74, 0x20, 0x73, 0x20, 0x65, 0x6d, 0x70, + 0x74, 0x79, 0x0a, 0x42, 0x65, 0x63, 0x61, 0x75, 0x73, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x79, 0x6f, 0x75, 0x0a, 0x49, + 0x20, 0x61, 0x6d, 0x20, 0x61, 0x66, 0x72, 0x61, 0x69, 0x64, 0x0a, 0x0a, 0x42, 0x65, 0x63, 0x61, 0x75, 0x73, 0x65, + 0x20, 0x6f, 0x66, 0x20, 0x79, 0x6f, 0x75, 0x0a, 0x42, 0x65, 0x63, 0x61, 0x75, 0x73, 0x65, 0x20, 0x6f, 0x66, 0x20, + 0x79, 0x6f, 0x75, 0x0a, 0x2e, 0x2e, 0x2e, 0x0a, /*0x0a, 0x66, 0x6f, 0x72, 0x20, 0x6a, 0x75, 0x6c, 0x79, 0x0a, 0x0a,*/ + }; + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/hidden/OptionsCommand.java b/core/src/main/java/com/taobao/arthas/core/command/hidden/OptionsCommand.java new file mode 100644 index 00000000..ae8aaecb --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/hidden/OptionsCommand.java @@ -0,0 +1,188 @@ +package com.taobao.arthas.core.command.hidden; + +import com.taobao.arthas.core.GlobalOptions; +import com.taobao.arthas.core.Option; +import com.taobao.arthas.core.command.Constants; +import com.taobao.arthas.core.shell.command.AnnotatedCommand; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.util.matcher.EqualsMatcher; +import com.taobao.arthas.core.util.matcher.Matcher; +import com.taobao.arthas.core.util.StringUtils; +import com.taobao.arthas.core.util.matcher.RegexMatcher; +import com.taobao.arthas.core.util.reflect.FieldUtils; +import com.taobao.middleware.cli.annotations.Argument; +import com.taobao.middleware.cli.annotations.Description; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Summary; +import com.taobao.text.Decoration; +import com.taobao.text.ui.Element; +import com.taobao.text.ui.TableElement; +import com.taobao.text.util.RenderUtil; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; + +import static com.taobao.arthas.core.util.ArthasCheckUtils.isIn; +import static com.taobao.text.ui.Element.label; +import static java.lang.String.format; + +/** + * 选项开关命令 + * + * @author vlinux on 15/6/6. + */ +@Name("options") +@Summary("View and change various Arthas options") +@Description(Constants.EXAMPLE + "options dump true\n"+ "options unsafe true\n" + + Constants.WIKI + Constants.WIKI_HOME + "cmds/options") +public class OptionsCommand extends AnnotatedCommand { + private String optionName; + private String optionValue; + + @Argument(index = 0, argName = "options-name", required = false) + @Description("Option name") + public void setOptionName(String optionName) { + this.optionName = optionName; + } + + @Argument(index = 1, argName = "options-value", required = false) + @Description("Option value") + public void setOptionValue(String optionValue) { + this.optionValue = optionValue; + } + + @Override + public void process(CommandProcess process) { + try { + if (isShow()) { + processShow(process); + } else if (isShowName()) { + processShowName(process); + } else { + processChangeNameValue(process); + } + } catch (Throwable t) { + // ignore + } finally { + process.end(); + } + } + + private void processShow(CommandProcess process) throws IllegalAccessException { + Collection fields = findOptions(new RegexMatcher(".*")); + process.write(RenderUtil.render(drawShowTable(fields), process.width())); + } + + private void processShowName(CommandProcess process) throws IllegalAccessException { + Collection fields = findOptions(new EqualsMatcher(optionName)); + process.write(RenderUtil.render(drawShowTable(fields), process.width())); + } + + private void processChangeNameValue(CommandProcess process) throws IllegalAccessException { + Collection fields = findOptions(new EqualsMatcher(optionName)); + + // name not exists + if (fields.isEmpty()) { + process.write(format("options[%s] not found.\n", optionName)); + return; + } + + Field field = fields.iterator().next(); + Option optionAnnotation = field.getAnnotation(Option.class); + Class type = field.getType(); + Object beforeValue = FieldUtils.readStaticField(field); + Object afterValue; + + try { + // try to case string to type + if (isIn(type, int.class, Integer.class)) { + FieldUtils.writeStaticField(field, afterValue = Integer.valueOf(optionValue)); + } else if (isIn(type, long.class, Long.class)) { + FieldUtils.writeStaticField(field, afterValue = Long.valueOf(optionValue)); + } else if (isIn(type, boolean.class, Boolean.class)) { + FieldUtils.writeStaticField(field, afterValue = Boolean.valueOf(optionValue)); + } else if (isIn(type, double.class, Double.class)) { + FieldUtils.writeStaticField(field, afterValue = Double.valueOf(optionValue)); + } else if (isIn(type, float.class, Float.class)) { + FieldUtils.writeStaticField(field, afterValue = Float.valueOf(optionValue)); + } else if (isIn(type, byte.class, Byte.class)) { + FieldUtils.writeStaticField(field, afterValue = Byte.valueOf(optionValue)); + } else if (isIn(type, short.class, Short.class)) { + FieldUtils.writeStaticField(field, afterValue = Short.valueOf(optionValue)); + } else if (isIn(type, short.class, String.class)) { + FieldUtils.writeStaticField(field, afterValue = optionValue); + } else { + process.write(format("Options[%s] type[%s] desupported.\n", optionName, type.getSimpleName())); + return; + } + + } catch (Throwable t) { + process.write(format("Cannot cast option value[%s] to type[%s].\n", optionValue, type.getSimpleName())); + return; + } + + TableElement table = new TableElement().leftCellPadding(1).rightCellPadding(1); + table.row(true, label("NAME").style(Decoration.bold.bold()), + label("BEFORE-VALUE").style(Decoration.bold.bold()), + label("AFTER-VALUE").style(Decoration.bold.bold())); + table.row(optionAnnotation.name(), StringUtils.objectToString(beforeValue), + StringUtils.objectToString(afterValue)); + process.write(RenderUtil.render(table, process.width())); + } + + + /* + * 判断当前动作是否需要展示整个options + */ + private boolean isShow() { + return StringUtils.isBlank(optionName) && StringUtils.isBlank(optionValue); + } + + + /* + * 判断当前动作是否需要展示某个Name的值 + */ + private boolean isShowName() { + return !StringUtils.isBlank(optionName) && StringUtils.isBlank(optionValue); + } + + private Collection findOptions(Matcher optionNameMatcher) { + final Collection matchFields = new ArrayList(); + for (final Field optionField : FieldUtils.getAllFields(GlobalOptions.class)) { + if (!optionField.isAnnotationPresent(Option.class)) { + continue; + } + + final Option optionAnnotation = optionField.getAnnotation(Option.class); + if (optionAnnotation != null + && !optionNameMatcher.matching(optionAnnotation.name())) { + continue; + } + matchFields.add(optionField); + } + return matchFields; + } + + private Element drawShowTable(Collection optionFields) throws IllegalAccessException { + TableElement table = new TableElement(1, 1, 2, 1, 3, 6) + .leftCellPadding(1).rightCellPadding(1); + table.row(true, label("LEVEL").style(Decoration.bold.bold()), + label("TYPE").style(Decoration.bold.bold()), + label("NAME").style(Decoration.bold.bold()), + label("VALUE").style(Decoration.bold.bold()), + label("SUMMARY").style(Decoration.bold.bold()), + label("DESCRIPTION").style(Decoration.bold.bold())); + + for (final Field optionField : optionFields) { + final Option optionAnnotation = optionField.getAnnotation(Option.class); + table.row("" + optionAnnotation.level(), + optionField.getType().getSimpleName(), + optionAnnotation.name(), + "" + optionField.get(null), + optionAnnotation.summary(), + optionAnnotation.description()); + } + return table; + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/hidden/ThanksCommand.java b/core/src/main/java/com/taobao/arthas/core/command/hidden/ThanksCommand.java new file mode 100644 index 00000000..d7b4adb7 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/hidden/ThanksCommand.java @@ -0,0 +1,24 @@ +package com.taobao.arthas.core.command.hidden; + +import com.taobao.arthas.core.shell.command.AnnotatedCommand; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.util.ArthasBanner; +import com.taobao.middleware.cli.annotations.Hidden; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Summary; + +/** + * 工具介绍
+ * 感谢 + * + * @author vlinux on 15/9/1. + */ +@Name("thanks") +@Summary("Credits to all personnel and organization who either contribute or help to this product. Thanks you all!") +@Hidden +public class ThanksCommand extends AnnotatedCommand { + @Override + public void process(CommandProcess process) { + process.write(ArthasBanner.credit()).write("\n").end(); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/klass100/ClassDumpTransformer.java b/core/src/main/java/com/taobao/arthas/core/command/klass100/ClassDumpTransformer.java new file mode 100644 index 00000000..5bf50a51 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/klass100/ClassDumpTransformer.java @@ -0,0 +1,77 @@ +package com.taobao.arthas.core.command.klass100; + +import com.taobao.arthas.core.util.FileUtils; +import com.taobao.arthas.core.util.LogUtil; +import com.taobao.middleware.logger.Logger; + +import java.io.File; +import java.io.IOException; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.security.ProtectionDomain; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * @author beiwei30 on 25/11/2016. + */ +class ClassDumpTransformer implements ClassFileTransformer { + + private static final Logger logger = LogUtil.getArthasLogger(); + + private Set> classesToEnhance; + private Map, File> dumpResult; + private File arthasLogHome; + + public ClassDumpTransformer(Set> classesToEnhance) { + this.classesToEnhance = classesToEnhance; + this.dumpResult = new HashMap, File>(); + this.arthasLogHome = new File(LogUtil.LOGGER_FILE).getParentFile(); + } + + @Override + public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, + ProtectionDomain protectionDomain, byte[] classfileBuffer) + throws IllegalClassFormatException { + if (classesToEnhance.contains(classBeingRedefined)) { + dumpClassIfNecessary(classBeingRedefined, classfileBuffer); + } + return null; + } + + public Map, File> getDumpResult() { + return dumpResult; + } + + private void dumpClassIfNecessary(Class clazz, byte[] data) { + String className = clazz.getName(); + ClassLoader classLoader = clazz.getClassLoader(); + String classDumpDir = "classdump"; + + // 创建类所在的包路径 + File dumpDir = new File(arthasLogHome, classDumpDir); + if (!dumpDir.mkdirs() && !dumpDir.exists()) { + logger.warn("create dump directory:{} failed.", dumpDir.getAbsolutePath()); + return; + } + + String fileName; + if (classLoader != null) { + fileName = classLoader.getClass().getName() + "-" + Integer.toHexString(classLoader.hashCode()) + + File.separator + className.replace(".", File.separator) + ".class"; + } else { + fileName = className.replace(".", File.separator) + ".class"; + } + + File dumpClassFile = new File(dumpDir, fileName); + + // 将类字节码写入文件 + try { + FileUtils.writeByteArrayToFile(dumpClassFile, data); + dumpResult.put(clazz, dumpClassFile); + } catch (IOException e) { + logger.warn("dump class:{} to file {} failed.", className, dumpClassFile, e); + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/klass100/ClassLoaderCommand.java b/core/src/main/java/com/taobao/arthas/core/command/klass100/ClassLoaderCommand.java new file mode 100644 index 00000000..bac0746a --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/klass100/ClassLoaderCommand.java @@ -0,0 +1,555 @@ +package com.taobao.arthas.core.command.klass100; + +import com.taobao.arthas.core.command.Constants; +import com.taobao.arthas.core.shell.command.AnnotatedCommand; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.util.affect.RowAffect; +import com.taobao.middleware.cli.annotations.Description; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Option; +import com.taobao.middleware.cli.annotations.Summary; +import com.taobao.text.Decoration; +import com.taobao.text.ui.Element; +import com.taobao.text.ui.LabelElement; +import com.taobao.text.ui.RowElement; +import com.taobao.text.ui.TableElement; +import com.taobao.text.ui.TreeElement; +import com.taobao.text.util.RenderUtil; + +import java.lang.instrument.Instrumentation; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; + +@Name("classloader") +@Summary("Show classloader info") +@Description(Constants.EXAMPLE + + " classloader\n" + + " classloader -t\n" + + " classloader -c 327a647b\n" + + " classloader -c 327a647b -r META-INF/MANIFEST.MF\n" + + " classloader -a\n" + + " classloader -a -c 327a647b\n" + + Constants.WIKI + Constants.WIKI_HOME + "cmds/classloader") +public class ClassLoaderCommand extends AnnotatedCommand { + private boolean isTree = false; + private String hashCode; + private boolean all = false; + private String resource; + private boolean includeReflectionClassLoader = true; + private boolean listClassLoader = false; + + @Option(shortName = "t", longName = "tree", flag = true) + @Description("Display ClassLoader tree") + public void setTree(boolean tree) { + isTree = tree; + } + + @Option(shortName = "c", longName = "classloader") + @Description("Display ClassLoader urls") + public void setHashCode(String hashCode) { + this.hashCode = hashCode; + } + + @Option(shortName = "a", longName = "all", flag = true) + @Description("Display all classes loaded by ClassLoader") + public void setAll(boolean all) { + this.all = all; + } + + @Option(shortName = "r", longName = "resource") + @Description("Use ClassLoader to find resources, won't work without -c specified") + public void setResource(String resource) { + this.resource = resource; + } + + @Option(shortName = "i", longName = "include-reflection-classloader", flag = true) + @Description("Include sun.reflect.DelegatingClassLoader") + public void setIncludeReflectionClassLoader(boolean includeReflectionClassLoader) { + this.includeReflectionClassLoader = includeReflectionClassLoader; + } + + @Option(shortName = "l", longName = "list-classloader", flag = true) + @Description("Display statistics info by classloader instance") + public void setListClassLoader(boolean listClassLoader) { + this.listClassLoader = listClassLoader; + } + + @Override + public void process(CommandProcess process) { + Instrumentation inst = process.session().getInstrumentation(); + if (all) { + processAllClasses(process, inst); + } else if (hashCode != null && resource != null) { + processResources(process, inst); + } else if (hashCode != null) { + processClassloader(process, inst); + } else if (listClassLoader || isTree){ + processClassloaders(process, inst); + } else { + processClassLoaderStats(process, inst); + } + } + + /** + * Calculate classloader statistics. + * e.g. In JVM, there are 100 GrooyClassLoader instances, which loaded 200 classes in total + * @param process + * @param inst + */ + private void processClassLoaderStats(CommandProcess process, Instrumentation inst) { + RowAffect affect = new RowAffect(); + List classLoaderInfos = getAllClassLoaderInfo(inst); + Map classLoaderStats = new HashMap(); + for (ClassLoaderInfo info: classLoaderInfos) { + String name = info.classLoader == null ? "BootstrapClassLoader" : info.classLoader.getClass().getName(); + ClassLoaderStat stat = classLoaderStats.get(name); + if (null == stat) { + stat = new ClassLoaderStat(); + classLoaderStats.put(name, stat); + } + stat.addLoadedCount(info.loadedClassCount); + stat.addNumberOfInstance(1); + } + + // sort the map by value + TreeMap sorted = + new TreeMap(new ValueComparator(classLoaderStats)); + sorted.putAll(classLoaderStats); + + Element element = renderStat(sorted); + process.write(RenderUtil.render(element, process.width())) + .write(com.taobao.arthas.core.util.Constants.EMPTY_STRING); + affect.rCnt(sorted.keySet().size()); + process.write(affect + "\n"); + process.end(); + } + + private void processClassloaders(CommandProcess process, Instrumentation inst) { + RowAffect affect = new RowAffect(); + List classLoaderInfos = includeReflectionClassLoader ? getAllClassLoaderInfo(inst) : + getAllClassLoaderInfo(inst, new SunReflectionClassLoaderFilter()); + Element element = isTree ? renderTree(classLoaderInfos) : renderTable(classLoaderInfos); + process.write(RenderUtil.render(element, process.width())) + .write(com.taobao.arthas.core.util.Constants.EMPTY_STRING); + affect.rCnt(classLoaderInfos.size()); + process.write(affect + "\n"); + process.end(); + } + + // 据 hashCode 来打印URLClassLoader的urls + private void processClassloader(CommandProcess process, Instrumentation inst) { + RowAffect affect = new RowAffect(); + + Set allClassLoader = getAllClassLoader(inst); + for (ClassLoader cl : allClassLoader) { + if (Integer.toHexString(cl.hashCode()).equals(hashCode)) { + process.write(RenderUtil.render(renderClassLoaderUrls(cl), process.width())); + } + } + process.write(com.taobao.arthas.core.util.Constants.EMPTY_STRING); + affect.rCnt(allClassLoader.size()); + process.write(affect + "\n"); + process.end(); + } + + // 使用ClassLoader去getResources + private void processResources(CommandProcess process, Instrumentation inst) { + RowAffect affect = new RowAffect(); + int rowCount = 0; + Set allClassLoader = includeReflectionClassLoader ? getAllClassLoader(inst) : + getAllClassLoader(inst, new SunReflectionClassLoaderFilter()); + for (ClassLoader cl : allClassLoader) { + if (Integer.toHexString(cl.hashCode()).equals(hashCode)) { + TableElement table = new TableElement().leftCellPadding(1).rightCellPadding(1); + try { + Enumeration urls = cl.getResources(resource); + while (urls.hasMoreElements()) { + URL url = urls.nextElement(); + table.row(url.toString()); + rowCount++; + } + } catch (Throwable e) { + e.printStackTrace(); + } + process.write(RenderUtil.render(table, process.width()) + "\n"); + } + } + process.write(com.taobao.arthas.core.util.Constants.EMPTY_STRING); + process.write(affect.rCnt(rowCount) + "\n"); + process.end(); + } + + private void processAllClasses(CommandProcess process, Instrumentation inst) { + RowAffect affect = new RowAffect(); + process.write(RenderUtil.render(renderClasses(hashCode, inst), process.width())); + process.write(affect + "\n"); + process.end(); + } + + /** + * 获取到所有的class, 还有它们的classloader,按classloader归类好,统一输出每个classloader里有哪些class + *

+ * 当hashCode是null,则把所有的classloader的都打印 + * + * @param hashCode + * @return + */ + @SuppressWarnings("rawtypes") + private static Element renderClasses(String hashCode, Instrumentation inst) { + int hashCodeInt = -1; + if (hashCode != null) { + hashCodeInt = Integer.valueOf(hashCode, 16); + } + + SortedSet bootstrapClassSet = new TreeSet(new Comparator() { + @Override + public int compare(Class o1, Class o2) { + return o1.getName().compareTo(o2.getName()); + } + }); + + Class[] allLoadedClasses = inst.getAllLoadedClasses(); + Map> classLoaderClassMap = new HashMap>(); + for (Class clazz : allLoadedClasses) { + ClassLoader classLoader = clazz.getClassLoader(); + // Class loaded by BootstrapClassLoader + if (classLoader == null) { + if (hashCode == null) { + bootstrapClassSet.add(clazz); + } + continue; + } + + if (hashCode != null && classLoader.hashCode() != hashCodeInt) { + continue; + } + + SortedSet classSet = classLoaderClassMap.get(classLoader); + if (classSet == null) { + classSet = new TreeSet(new Comparator() { + @Override + public int compare(Class o1, Class o2) { + return o1.getName().compareTo(o2.getName()); + } + }); + classLoaderClassMap.put(classLoader, classSet); + } + classSet.add(clazz); + } + + TableElement table = new TableElement().leftCellPadding(1).rightCellPadding(1); + + if (!bootstrapClassSet.isEmpty()) { + table.row(new LabelElement("hash:null, BootstrapClassLoader").style(Decoration.bold.bold())); + for (Class clazz : bootstrapClassSet) { + table.row(new LabelElement(clazz.getName())); + } + table.row(new LabelElement(" ")); + } + + for (Entry> entry : classLoaderClassMap.entrySet()) { + ClassLoader classLoader = entry.getKey(); + SortedSet classSet = entry.getValue(); + + table.row(new LabelElement("hash:" + classLoader.hashCode() + ", " + classLoader.toString()) + .style(Decoration.bold.bold())); + for (Class clazz : classSet) { + table.row(new LabelElement(clazz.getName())); + } + + table.row(new LabelElement(" ")); + } + return table; + } + + private static Element renderClassLoaderUrls(ClassLoader classLoader) { + StringBuilder sb = new StringBuilder(); + if (classLoader instanceof URLClassLoader) { + URLClassLoader cl = (URLClassLoader) classLoader; + URL[] urls = cl.getURLs(); + if (urls != null) { + for (URL url : urls) { + sb.append(url.toString() + "\n"); + } + return new LabelElement(sb.toString()); + } else { + return new LabelElement("urls is empty."); + } + } else { + return new LabelElement("not a URLClassLoader.\n"); + } + } + + // 以树状列出ClassLoader的继承结构 + private static Element renderTree(List classLoaderInfos) { + TreeElement root = new TreeElement(); + + List parentNullClassLoaders = new ArrayList(); + List parentNotNullClassLoaders = new ArrayList(); + for (ClassLoaderInfo info : classLoaderInfos) { + if (info.parent() == null) { + parentNullClassLoaders.add(info); + } else { + parentNotNullClassLoaders.add(info); + } + } + + for (ClassLoaderInfo info : parentNullClassLoaders) { + if (info.parent() == null) { + TreeElement parent = new TreeElement(info.getName()); + renderParent(parent, info, parentNotNullClassLoaders); + root.addChild(parent); + } + } + + return root; + } + + // 统计所有的ClassLoader的信息 + private static TableElement renderTable(List classLoaderInfos) { + TableElement table = new TableElement().leftCellPadding(1).rightCellPadding(1); + table.add(new RowElement().style(Decoration.bold.bold()).add("name", "loadedCount", "hash", "parent")); + for (ClassLoaderInfo info : classLoaderInfos) { + table.row(info.getName(), "" + info.loadedClassCount(), info.hashCodeStr(), info.parentStr()); + } + return table; + } + + private static TableElement renderStat(Map classLoaderStats) { + TableElement table = new TableElement().leftCellPadding(1).rightCellPadding(1); + table.add(new RowElement().style(Decoration.bold.bold()).add("name", "numberOfInstances", "loadedCountTotal")); + for (Map.Entry entry : classLoaderStats.entrySet()) { + table.row(entry.getKey(), "" + entry.getValue().getNumberOfInstance(), "" + entry.getValue().getLoadedCount()); + } + return table; + } + + private static void renderParent(TreeElement node, ClassLoaderInfo parent, List classLoaderInfos) { + for (ClassLoaderInfo info : classLoaderInfos) { + if (info.parent() == parent.classLoader) { + TreeElement child = new TreeElement(info.getName()); + node.addChild(child); + renderParent(child, info, classLoaderInfos); + } + } + } + + private static Set getAllClassLoader(Instrumentation inst, Filter... filters) { + Set classLoaderSet = new HashSet(); + + for (Class clazz : inst.getAllLoadedClasses()) { + ClassLoader classLoader = clazz.getClassLoader(); + if (classLoader != null) { + if (shouldInclude(classLoader, filters)) { + classLoaderSet.add(classLoader); + } + } + } + return classLoaderSet; + } + + private static List getAllClassLoaderInfo(Instrumentation inst, Filter... filters) { + // 这里认为class.getClassLoader()返回是null的是由BootstrapClassLoader加载的,特殊处理 + ClassLoaderInfo bootstrapInfo = new ClassLoaderInfo(null); + + Map loaderInfos = new HashMap(); + + for (Class clazz : inst.getAllLoadedClasses()) { + ClassLoader classLoader = clazz.getClassLoader(); + if (classLoader == null) { + bootstrapInfo.increase(); + } else { + if (shouldInclude(classLoader, filters)) { + ClassLoaderInfo loaderInfo = loaderInfos.get(classLoader); + if (loaderInfo == null) { + loaderInfo = new ClassLoaderInfo(classLoader); + loaderInfos.put(classLoader, loaderInfo); + ClassLoader parent = classLoader.getParent(); + while (parent != null) { + ClassLoaderInfo parentLoaderInfo = loaderInfos.get(parent); + if (parentLoaderInfo == null) { + parentLoaderInfo = new ClassLoaderInfo(parent); + loaderInfos.put(parent, parentLoaderInfo); + } + parent = parent.getParent(); + } + } + loaderInfo.increase(); + } + } + } + + // 排序时,把用户自己定的ClassLoader排在最前面,以sun. + // 开头的放后面,因为sun.reflect.DelegatingClassLoader的实例太多 + List sunClassLoaderList = new ArrayList(); + + List otherClassLoaderList = new ArrayList(); + + for (Entry entry : loaderInfos.entrySet()) { + ClassLoader classLoader = entry.getKey(); + if (classLoader.getClass().getName().startsWith("sun.")) { + sunClassLoaderList.add(entry.getValue()); + } else { + otherClassLoaderList.add(entry.getValue()); + } + } + + Collections.sort(sunClassLoaderList); + Collections.sort(otherClassLoaderList); + + List result = new ArrayList(); + result.add(bootstrapInfo); + result.addAll(otherClassLoaderList); + result.addAll(sunClassLoaderList); + return result; + } + + private static boolean shouldInclude(ClassLoader classLoader, Filter... filters) { + if (filters == null) { + return true; + } + + for (Filter filter : filters) { + if (!filter.accept(classLoader)) { + return false; + } + } + return true; + } + + private static class ClassLoaderInfo implements Comparable { + private ClassLoader classLoader; + private int loadedClassCount = 0; + + ClassLoaderInfo(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + public ClassLoader getClassLoader() { + return classLoader; + } + + public String getName() { + if (classLoader != null) { + return classLoader.toString(); + } + return "BootstrapClassLoader"; + } + + String hashCodeStr() { + if (classLoader != null) { + return "" + Integer.toHexString(classLoader.hashCode()); + } + return "null"; + } + + void increase() { + loadedClassCount++; + } + + int loadedClassCount() { + return loadedClassCount; + } + + ClassLoader parent() { + return classLoader == null ? null : classLoader.getParent(); + } + + String parentStr() { + if (classLoader == null) { + return "null"; + } + ClassLoader parent = classLoader.getParent(); + if (parent == null) { + return "null"; + } + return parent.toString(); + } + + @Override + public int compareTo(ClassLoaderInfo other) { + if (other == null) { + return -1; + } + if (other.classLoader == null) { + return -1; + } + if (this.classLoader == null) { + return -1; + } + + return this.classLoader.getClass().getName().compareTo(other.classLoader.getClass().getName()); + } + + } + + private interface Filter { + boolean accept(ClassLoader classLoader); + } + + private static class SunReflectionClassLoaderFilter implements Filter { + private static final String REFLECTION_CLASSLOADER = "sun.reflect.DelegatingClassLoader"; + + @Override + public boolean accept(ClassLoader classLoader) { + return !REFLECTION_CLASSLOADER.equals(classLoader.getClass().getName()); + } + } + + private static class ClassLoaderStat { + private int loadedCount; + private int numberOfInstance; + + void addLoadedCount(int count) { + this.loadedCount += count; + } + + void addNumberOfInstance(int count) { + this.numberOfInstance += count; + } + + int getLoadedCount() { + return loadedCount; + } + + int getNumberOfInstance() { + return numberOfInstance; + } + } + + private static class ValueComparator implements Comparator { + + private Map unsortedStats; + + ValueComparator(Map stats) { + this.unsortedStats = stats; + } + + @Override + public int compare(String o1, String o2) { + if (null == unsortedStats) { + return -1; + } + if (!unsortedStats.containsKey(o1)) { + return 1; + } + if (!unsortedStats.containsKey(o2)) { + return -1; + } + return unsortedStats.get(o2).getLoadedCount() - unsortedStats.get(o1).getLoadedCount(); + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/klass100/DumpClassCommand.java b/core/src/main/java/com/taobao/arthas/core/command/klass100/DumpClassCommand.java new file mode 100644 index 00000000..e68e53d2 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/klass100/DumpClassCommand.java @@ -0,0 +1,141 @@ +package com.taobao.arthas.core.command.klass100; + +import com.taobao.arthas.core.advisor.Enhancer; +import com.taobao.arthas.core.command.Constants; +import com.taobao.arthas.core.shell.command.AnnotatedCommand; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.util.LogUtil; +import com.taobao.arthas.core.util.SearchUtils; +import com.taobao.arthas.core.util.StringUtils; +import com.taobao.arthas.core.util.TypeRenderUtils; +import com.taobao.arthas.core.util.affect.RowAffect; +import com.taobao.middleware.cli.annotations.Argument; +import com.taobao.middleware.cli.annotations.Description; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Option; +import com.taobao.middleware.cli.annotations.Summary; +import com.taobao.middleware.logger.Logger; +import com.taobao.text.Color; +import com.taobao.text.Decoration; +import com.taobao.text.ui.Element; +import com.taobao.text.ui.LabelElement; +import com.taobao.text.ui.TableElement; +import com.taobao.text.util.RenderUtil; + +import java.io.File; +import java.lang.instrument.Instrumentation; +import java.lang.instrument.UnmodifiableClassException; +import java.util.Map; +import java.util.Set; + +import static com.taobao.text.ui.Element.label; + +/** + * Dump class byte array + */ +@Name("dump") +@Summary("Dump class byte array from JVM") +@Description(Constants.EXAMPLE + + " dump -E org\\\\.apache\\\\.commons\\\\.lang\\\\.StringUtils\n" + + " dump org.apache.commons.lang.StringUtils\n" + + " dump org/apache/commons/lang/StringUtils\n" + + " dump *StringUtils\n" + + Constants.WIKI + Constants.WIKI_HOME + "cmds/dump") +public class DumpClassCommand extends AnnotatedCommand { + private static final Logger logger = LogUtil.getArthasLogger(); + + private String classPattern; + private String code = null; + private boolean isRegEx = false; + + @Argument(index = 0, argName = "class-pattern") + @Description("Class name pattern, use either '.' or '/' as separator") + public void setClassPattern(String classPattern) { + this.classPattern = classPattern; + } + + @Option(shortName = "c", longName = "code") + @Description("The hash code of the special class's classLoader") + public void setCode(String code) { + this.code = code; + } + + @Option(shortName = "E", longName = "regex") + @Description("Enable regular expression to match (wildcard matching by default)") + public void setRegEx(boolean regEx) { + isRegEx = regEx; + } + + @Override + public void process(CommandProcess process) { + RowAffect effect = new RowAffect(); + Instrumentation inst = process.session().getInstrumentation(); + + Set> matchedClasses = SearchUtils.searchClass(inst, classPattern, isRegEx, code); + try { + if (matchedClasses == null || matchedClasses.isEmpty()) { + processNoMatch(process); + } else if (matchedClasses.size() > 5) { + processMatches(process, matchedClasses); + } else { + processMatch(process, effect, inst, matchedClasses); + } + } finally { + process.write(effect + "\n"); + process.end(); + } + } + + + private void processMatch(CommandProcess process, RowAffect effect, Instrumentation inst, Set> matchedClasses) { + try { + Map, File> classFiles = dump(inst, matchedClasses); + TableElement table = new TableElement().leftCellPadding(1).rightCellPadding(1); + table.row(new LabelElement("HASHCODE").style(Decoration.bold.bold()), + new LabelElement("CLASSLOADER").style(Decoration.bold.bold()), + new LabelElement("LOCATION").style(Decoration.bold.bold())); + + for (Map.Entry, File> entry : classFiles.entrySet()) { + Class clazz = entry.getKey(); + File file = entry.getValue(); + table.row(label(StringUtils.classLoaderHash(clazz)).style(Decoration.bold.fg(Color.red)), + TypeRenderUtils.drawClassLoader(clazz), + label(file.getCanonicalPath()).style(Decoration.bold.fg(Color.red))); + } + + process.write(RenderUtil.render(table, process.width())) + .write(com.taobao.arthas.core.util.Constants.EMPTY_STRING); + effect.rCnt(classFiles.keySet().size()); + } catch (Throwable t) { + logger.error(null, "dump: fail to dump classes: " + matchedClasses, t); + } + } + + private void processMatches(CommandProcess process, Set> matchedClasses) { + Element usage = new LabelElement("dump -c hashcode " + classPattern).style(Decoration.bold.fg(Color.blue)); + process.write("Found more than 5 class for: " + classPattern + ", Please use "); + process.write(RenderUtil.render(usage, process.width())); + + TableElement table = new TableElement().leftCellPadding(1).rightCellPadding(1); + table.row(new LabelElement("NAME").style(Decoration.bold.bold()), + new LabelElement("HASHCODE").style(Decoration.bold.bold()), + new LabelElement("CLASSLOADER").style(Decoration.bold.bold())); + + for (Class c : matchedClasses) { + table.row(label(c.getName()), label(StringUtils.classLoaderHash(c)).style(Decoration.bold.fg(Color.red)), + TypeRenderUtils.drawClassLoader(c)); + } + + process.write(RenderUtil.render(table, process.width()) + "\n"); + } + + private void processNoMatch(CommandProcess process) { + process.write("No class found for: " + classPattern + "\n"); + } + + private Map, File> dump(Instrumentation inst, Set> classes) throws UnmodifiableClassException { + ClassDumpTransformer transformer = new ClassDumpTransformer(classes); + Enhancer.enhance(inst, transformer, classes); + return transformer.getDumpResult(); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/klass100/GetStaticCommand.java b/core/src/main/java/com/taobao/arthas/core/command/klass100/GetStaticCommand.java new file mode 100644 index 00000000..3591043b --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/klass100/GetStaticCommand.java @@ -0,0 +1,180 @@ +package com.taobao.arthas.core.command.klass100; + +import com.taobao.arthas.core.command.Constants; +import com.taobao.arthas.core.command.express.ExpressException; +import com.taobao.arthas.core.command.express.ExpressFactory; +import com.taobao.arthas.core.shell.command.AnnotatedCommand; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.util.LogUtil; +import com.taobao.arthas.core.util.SearchUtils; +import com.taobao.arthas.core.util.StringUtils; +import com.taobao.arthas.core.util.TypeRenderUtils; +import com.taobao.arthas.core.util.affect.RowAffect; +import com.taobao.arthas.core.util.matcher.Matcher; +import com.taobao.arthas.core.util.matcher.RegexMatcher; +import com.taobao.arthas.core.util.matcher.WildcardMatcher; +import com.taobao.arthas.core.view.ObjectView; +import com.taobao.middleware.cli.annotations.Argument; +import com.taobao.middleware.cli.annotations.Description; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Option; +import com.taobao.middleware.cli.annotations.Summary; +import com.taobao.middleware.logger.Logger; +import com.taobao.text.Color; +import com.taobao.text.Decoration; +import com.taobao.text.ui.Element; +import com.taobao.text.ui.LabelElement; +import com.taobao.text.ui.TableElement; +import com.taobao.text.util.RenderUtil; + +import java.lang.instrument.Instrumentation; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Set; + +import static com.taobao.text.ui.Element.label; + +/** + * @author diecui1202 on 2017/9/27. + */ + +@Name("getstatic") +@Summary("Show the static field of a class") +@Description(Constants.EXAMPLE + " getstatic -c 39eb305e org.apache.log4j.LogManager DEFAULT_CONFIGURATION_FILE\n" + + Constants.WIKI + Constants.WIKI_HOME + "cmds/getstatic") +public class GetStaticCommand extends AnnotatedCommand { + + private static final Logger logger = LogUtil.getArthasLogger(); + + private String classPattern; + private String fieldPattern; + private String express; + private String code = null; + private boolean isRegEx = false; + private int expand = 1; + + @Argument(argName = "class-pattern", index = 0) + @Description("Class name pattern, use either '.' or '/' as separator") + public void setClassPattern(String classPattern) { + this.classPattern = classPattern; + } + + @Argument(argName = "field-pattern", index = 1) + @Description("Field name pattern") + public void setFieldPattern(String fieldPattern) { + this.fieldPattern = fieldPattern; + } + + @Argument(argName = "express", index = 2, required = false) + @Description("the content you want to watch, written by ognl") + public void setExpress(String express) { + this.express = express; + } + + @Option(shortName = "c", longName = "code") + @Description("The hash code of the special class's classLoader") + public void setCode(String code) { + this.code = code; + } + + @Option(shortName = "E", longName = "regex", flag = true) + @Description("Enable regular expression to match (wildcard matching by default)") + public void setRegEx(boolean regEx) { + isRegEx = regEx; + } + + @Option(shortName = "x", longName = "expand") + @Description("Expand level of object (1 by default)") + public void setExpand(Integer expand) { + this.expand = expand; + } + + @Override + public void process(CommandProcess process) { + RowAffect affect = new RowAffect(); + Instrumentation inst = process.session().getInstrumentation(); + Set> matchedClasses = SearchUtils.searchClassOnly(inst, classPattern, isRegEx, code); + + try { + if (matchedClasses == null || matchedClasses.isEmpty()) { + process.write("No class found for: " + classPattern + "\n"); + } else if (matchedClasses.size() > 1) { + processMatches(process, matchedClasses); + } else { + processExactMatch(process, affect, inst, matchedClasses); + } + } finally { + process.write(affect + "\n"); + process.end(); + } + } + + private void processExactMatch(CommandProcess process, RowAffect affect, Instrumentation inst, + Set> matchedClasses) { + Matcher fieldNameMatcher = fieldNameMatcher(); + + Class clazz = matchedClasses.iterator().next(); + + boolean found = false; + + for (Field field : clazz.getDeclaredFields()) { + if (!Modifier.isStatic(field.getModifiers()) || !fieldNameMatcher.matching(field.getName())) { + continue; + } + if (!field.isAccessible()) { + field.setAccessible(true); + } + try { + Object value = field.get(null); + + if (!StringUtils.isEmpty(express)) { + value = ExpressFactory.newExpress(value).get(express); + } + + String result = StringUtils.objectToString(expand >= 0 ? new ObjectView(value, expand).draw() : value); + process.write("field: " + field.getName() + "\n" + result + "\n"); + + affect.rCnt(1); + } catch (IllegalAccessException e) { + logger.warn("getstatic: failed to get static value, class: " + clazz + ", field: " + field.getName(), + e); + process.write("Failed to get static, exception message: " + e.getMessage() + + ", please check $HOME/logs/arthas/arthas.log for more details. \n"); + } catch (ExpressException e) { + logger.warn("getstatic: failed to get express value, class: " + clazz + ", field: " + field.getName() + + ", express: " + express, e); + process.write("Failed to get static, exception message: " + e.getMessage() + + ", please check $HOME/logs/arthas/arthas.log for more details. \n"); + } finally { + found = true; + } + } + + if (!found) { + process.write("getstatic: no matched static field was found\n"); + } + } + + private void processMatches(CommandProcess process, Set> matchedClasses) { + Element usage = new LabelElement("getstatic -c " + classPattern + " " + fieldPattern).style( + Decoration.bold.fg(Color.blue)); + process.write("\n Found more than one class for: " + classPattern + ", Please use " + RenderUtil.render(usage, + process.width())); + + TableElement table = new TableElement().leftCellPadding(1).rightCellPadding(1); + table.row(new LabelElement("HASHCODE").style(Decoration.bold.bold()), + new LabelElement("CLASSLOADER").style(Decoration.bold.bold())); + + for (Class c : matchedClasses) { + ClassLoader classLoader = c.getClassLoader(); + table.row(label(Integer.toHexString(classLoader.hashCode())).style(Decoration.bold.fg(Color.red)), + TypeRenderUtils.drawClassLoader(c)); + } + + process.write(RenderUtil.render(table, process.width()) + "\n"); + } + + private Matcher fieldNameMatcher() { + return isRegEx ? new RegexMatcher(fieldPattern) : new WildcardMatcher(fieldPattern); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/klass100/JadCommand.java b/core/src/main/java/com/taobao/arthas/core/command/klass100/JadCommand.java new file mode 100644 index 00000000..5132d60e --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/klass100/JadCommand.java @@ -0,0 +1,204 @@ +package com.taobao.arthas.core.command.klass100; + +import com.taobao.arthas.core.advisor.Enhancer; +import com.taobao.arthas.core.command.Constants; +import com.taobao.arthas.core.shell.command.AnnotatedCommand; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.util.FileUtils; +import com.taobao.arthas.core.util.LogUtil; +import com.taobao.arthas.core.util.SearchUtils; +import com.taobao.arthas.core.util.TypeRenderUtils; +import com.taobao.arthas.core.util.affect.RowAffect; +import com.taobao.middleware.cli.annotations.Argument; +import com.taobao.middleware.cli.annotations.Description; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Option; +import com.taobao.middleware.cli.annotations.Summary; +import com.taobao.middleware.logger.Logger; +import com.taobao.text.Color; +import com.taobao.text.Decoration; +import com.taobao.text.lang.LangRenderUtil; +import com.taobao.text.ui.Element; +import com.taobao.text.ui.LabelElement; +import com.taobao.text.ui.TableElement; +import com.taobao.text.util.RenderUtil; +import org.benf.cfr.reader.Main; +import org.objectweb.asm.Type; + +import java.io.File; +import java.io.IOException; +import java.lang.instrument.Instrumentation; +import java.nio.charset.Charset; +import java.util.*; +import java.util.regex.Pattern; + +import static com.taobao.text.ui.Element.label; + +/** + * @author diecui1202 on 15/11/24. + */ +@Name("jad") +@Summary("Decompile class") +@Description(Constants.EXAMPLE + + " jad -c 39eb305e org.apache.log4j.Logger\n" + + " jad -c 39eb305e org/apache/log4j/Logger\n" + + " jad -c 39eb305e -E org\\\\.apache\\\\.*\\\\.StringUtils\n" + + Constants.WIKI + Constants.WIKI_HOME + "cmds/jad") +public class JadCommand extends AnnotatedCommand { + private static final Logger logger = LogUtil.getArthasLogger(); + private static Pattern pattern = Pattern.compile("(?m)^/\\*\\s*\\*/\\s*$" + System.getProperty("line.separator")); + private static final String OUTPUTOPTION = "--outputdir"; + private static final String COMMENTS = "--comments"; + private static final String DecompilePath = new File(LogUtil.LOGGER_FILE).getParent() + File.separator + "decompile"; + + private String classPattern; + private String methodName; + private String code = null; + private boolean isRegEx = false; + + @Argument(argName = "class-pattern", index = 0) + @Description("Class name pattern, use either '.' or '/' as separator") + public void setClassPattern(String classPattern) { + this.classPattern = classPattern; + } + + @Argument(argName = "method-name", index = 1, required = false) + @Description("method name pattern, decompile a specific method instead of the whole class") + public void setMethodName(String methodName) { + this.methodName = methodName; + } + + + @Option(shortName = "c", longName = "code") + @Description("The hash code of the special class's classLoader") + public void setCode(String code) { + this.code = code; + } + + @Option(shortName = "E", longName = "regex", flag = true) + @Description("Enable regular expression to match (wildcard matching by default)") + public void setRegEx(boolean regEx) { + isRegEx = regEx; + } + + @Override + public void process(CommandProcess process) { + RowAffect affect = new RowAffect(); + Instrumentation inst = process.session().getInstrumentation(); + Set> matchedClasses = SearchUtils.searchClassOnly(inst, classPattern, isRegEx, code); + + try { + if (matchedClasses == null || matchedClasses.isEmpty()) { + processNoMatch(process); + } else if (matchedClasses.size() > 1) { + processMatches(process, matchedClasses); + } else { + Set> withInnerClasses = SearchUtils.searchClassOnly(inst, classPattern + "(?!.*\\$\\$Lambda\\$).*", true, code); + processExactMatch(process, affect, inst, matchedClasses, withInnerClasses); + } + } finally { + process.write(affect + "\n"); + process.end(); + } + } + + private void processExactMatch(CommandProcess process, RowAffect affect, Instrumentation inst, Set> matchedClasses, Set> withInnerClasses) { + Class c = matchedClasses.iterator().next(); + matchedClasses = withInnerClasses; + + try { + ClassDumpTransformer transformer = new ClassDumpTransformer(matchedClasses); + Enhancer.enhance(inst, transformer, matchedClasses); + Map, File> classFiles = transformer.getDumpResult(); + File classFile = classFiles.get(c); + + String source; + source = decompileWithCFR(classFile.getAbsolutePath(), c, methodName); + if (source != null) { + source = pattern.matcher(source).replaceAll(""); + } else { + source = "unknown"; + } + + + process.write("\n"); + process.write(RenderUtil.render(new LabelElement("ClassLoader: ").style(Decoration.bold.fg(Color.red)), process.width())); + process.write(RenderUtil.render(TypeRenderUtils.drawClassLoader(c), process.width()) + "\n"); + process.write(RenderUtil.render(new LabelElement("Location: ").style(Decoration.bold.fg(Color.red)), process.width())); + process.write(RenderUtil.render(new LabelElement(SearchClassCommand.getCodeSource( + c.getProtectionDomain().getCodeSource())).style(Decoration.bold.fg(Color.blue)), process.width()) + "\n"); + process.write(LangRenderUtil.render(source) + "\n"); + process.write(com.taobao.arthas.core.util.Constants.EMPTY_STRING); + affect.rCnt(classFiles.keySet().size()); + } catch (Throwable t) { + logger.error(null, "jad: fail to decompile class: " + c.getName(), t); + } + } + + private void processMatches(CommandProcess process, Set> matchedClasses) { + Element usage = new LabelElement("jad -c " + classPattern).style(Decoration.bold.fg(Color.blue)); + process.write("\n Found more than one class for: " + classPattern + ", Please use " + + RenderUtil.render(usage, process.width())); + + TableElement table = new TableElement().leftCellPadding(1).rightCellPadding(1); + table.row(new LabelElement("HASHCODE").style(Decoration.bold.bold()), + new LabelElement("CLASSLOADER").style(Decoration.bold.bold())); + + for (Class c : matchedClasses) { + ClassLoader classLoader = c.getClassLoader(); + table.row(label(Integer.toHexString(classLoader.hashCode())).style(Decoration.bold.fg(Color.red)), + TypeRenderUtils.drawClassLoader(c)); + } + + process.write(RenderUtil.render(table, process.width()) + "\n"); + } + + private void processNoMatch(CommandProcess process) { + process.write("No class found for: " + classPattern + "\n"); + } + + private String decompileWithCFR(String classPath, Class clazz, String methodName) { + List options = new ArrayList(); + options.add(classPath); +// options.add(clazz.getName()); + if (methodName != null) { + options.add(methodName); + } + options.add(OUTPUTOPTION); + options.add(DecompilePath); + options.add(COMMENTS); + options.add("false"); + String args[] = new String[options.size()]; + options.toArray(args); + Main.main(args); + String outputFilePath = DecompilePath + File.separator + Type.getInternalName(clazz) + ".java"; + File outputFile = new File(outputFilePath); + if (outputFile.exists()) { + try { + return FileUtils.readFileToString(outputFile, Charset.defaultCharset()); + } catch (IOException e) { + logger.error(null, "error read decompile result in: " + outputFilePath, e); + } + } + + return null; + } + + public static void main(String[] args) { + String[] names = { + "com.taobao.container.web.arthas.mvc.AppInfoController", + "com.taobao.container.web.arthas.mvc.AppInfoController$1$$Lambda$19/381016128", + "com.taobao.container.web.arthas.mvc.AppInfoController$$Lambda$16/17741163", + "com.taobao.container.web.arthas.mvc.AppInfoController$1", + "com.taobao.container.web.arthas.mvc.AppInfoController$123", + "com.taobao.container.web.arthas.mvc.AppInfoController$A", + "com.taobao.container.web.arthas.mvc.AppInfoController$ABC" + }; + + String pattern = "com.taobao.container.web.arthas.mvc.AppInfoController" + "(?!.*\\$\\$Lambda\\$).*"; + for(String name : names) { + System.out.println(name + " " + Pattern.matches(pattern, name)); + } + + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/klass100/RedefineCommand.java b/core/src/main/java/com/taobao/arthas/core/command/klass100/RedefineCommand.java new file mode 100644 index 00000000..a41f91c4 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/klass100/RedefineCommand.java @@ -0,0 +1,137 @@ +package com.taobao.arthas.core.command.klass100; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.lang.instrument.ClassDefinition; +import java.lang.instrument.Instrumentation; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.objectweb.asm.ClassReader; + +import com.taobao.arthas.core.command.Constants; +import com.taobao.arthas.core.shell.command.AnnotatedCommand; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.middleware.cli.annotations.Description; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Option; +import com.taobao.middleware.cli.annotations.Summary; + +/** + * Redefine Classes. + * + * @author hengyunabc 2018-07-13 + * @see java.lang.instrument.Instrumentation#redefineClasses(ClassDefinition...) + */ +@Name("redefine") +@Summary("Redefine classes. @see Instrumentation#redefineClasses(ClassDefinition...)") +@Description(Constants.EXAMPLE + + " redefine -p /tmp/Test.class\n" + + " redefine -c 327a647b -p /tmp/Test.class /tmp/Test\\$Inner.class \n" + + Constants.WIKI + Constants.WIKI_HOME + "cmds/redefine") +public class RedefineCommand extends AnnotatedCommand { + + private static final int MAX_FILE_SIZE = 10 * 1024 * 1024; + + private String hashCode; + + private List paths; + + @Option(shortName = "c", longName = "classloader") + @Description("classLoader hashcode") + public void setHashCode(String hashCode) { + this.hashCode = hashCode; + } + + @Option(shortName = "p", longName = "path", acceptMultipleValues = true) + @Description(".class file paths") + public void setPathPatterns(List paths) { + this.paths = paths; + } + + @Override + public void process(CommandProcess process) { + if (paths == null || paths.isEmpty()) { + process.write("paths is empty.\n"); + process.end(); + return; + } + Instrumentation inst = process.session().getInstrumentation(); + + for (String path : paths) { + File file = new File(path); + if (!file.exists()) { + process.write("path is not exists, path:" + path + "\n"); + process.end(); + return; + } + if (!file.isFile()) { + process.write("path is not a normal file, path:" + path + "\n"); + process.end(); + return; + } + if (file.length() >= MAX_FILE_SIZE) { + process.write("file size: " + file.length() + " >= " + MAX_FILE_SIZE + ", path: " + path + "\n"); + process.end(); + return; + } + } + + Map bytesMap = new HashMap(); + for (String path : paths) { + RandomAccessFile f = null; + try { + f = new RandomAccessFile(path, "r"); + final byte[] bytes = new byte[(int) f.length()]; + f.readFully(bytes); + + final String clazzName = readClassName(bytes); + + bytesMap.put(clazzName, bytes); + + } catch (Exception e) { + process.write("" + e + "\n"); + } finally { + if (f != null) { + try { + f.close(); + } catch (IOException e) { + // ignore + } + } + } + } + + if (bytesMap.size() != paths.size()) { + process.write("paths may contains same class name!\n"); + process.end(); + return; + } + + List definitions = new ArrayList(); + for (Class clazz : inst.getAllLoadedClasses()) { + if (bytesMap.containsKey(clazz.getName())) { + if (hashCode != null && !Integer.toHexString(clazz.getClassLoader().hashCode()).equals(hashCode)) { + continue; + } + definitions.add(new ClassDefinition(clazz, bytesMap.get(clazz.getName()))); + } + } + + try { + inst.redefineClasses(definitions.toArray(new ClassDefinition[0])); + process.write("redefine success, size: " + definitions.size() + "\n"); + } catch (Exception e) { + process.write("redefine error! " + e + "\n"); + } + + process.end(); + } + + private static String readClassName(final byte[] bytes) { + return new ClassReader(bytes).getClassName().replace("/", "."); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/klass100/SearchClassCommand.java b/core/src/main/java/com/taobao/arthas/core/command/klass100/SearchClassCommand.java new file mode 100644 index 00000000..14d7ee37 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/klass100/SearchClassCommand.java @@ -0,0 +1,138 @@ +package com.taobao.arthas.core.command.klass100; + +import com.taobao.arthas.core.command.Constants; +import com.taobao.arthas.core.shell.command.AnnotatedCommand; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.util.SearchUtils; +import com.taobao.arthas.core.util.StringUtils; +import com.taobao.arthas.core.util.TypeRenderUtils; +import com.taobao.arthas.core.util.affect.RowAffect; +import com.taobao.middleware.cli.annotations.Argument; +import com.taobao.middleware.cli.annotations.Description; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Option; +import com.taobao.middleware.cli.annotations.Summary; +import com.taobao.text.Decoration; +import com.taobao.text.ui.Element; +import com.taobao.text.ui.TableElement; +import com.taobao.text.util.RenderUtil; + +import java.lang.instrument.Instrumentation; +import java.security.CodeSource; +import java.util.Set; + +import static com.taobao.text.ui.Element.label; + +/** + * 展示类信息 + * + * @author vlinux + */ +@Name("sc") +@Summary("Search all the classes loaded by JVM") +@Description(Constants.EXAMPLE + + " sc -E org\\\\.apache\\\\.commons\\\\.lang\\\\.StringUtils\n" + + " sc -d org.apache.commons.lang.StringUtils\n" + + " sc -d org/apache/commons/lang/StringUtils\n" + + " sc -d *StringUtils\n" + + Constants.WIKI + Constants.WIKI_HOME + "cmds/sc") +public class SearchClassCommand extends AnnotatedCommand { + private String classPattern; + private boolean isDetail = false; + private boolean isField = false; + private boolean isRegEx = false; + private Integer expand; + + @Argument(argName = "class-pattern", index = 0) + @Description("Class name pattern, use either '.' or '/' as separator") + public void setClassPattern(String classPattern) { + this.classPattern = classPattern; + } + + @Option(shortName = "d", longName = "details", flag = true) + @Description("Display the details of class") + public void setDetail(boolean detail) { + isDetail = detail; + } + + @Option(shortName = "f", longName = "field", flag = true) + @Description("Display all the member variables") + public void setField(boolean field) { + isField = field; + } + + @Option(shortName = "E", longName = "regex", flag = true) + @Description("Enable regular expression to match (wildcard matching by default)") + public void setRegEx(boolean regEx) { + isRegEx = regEx; + } + + @Option(shortName = "x", longName = "expand") + @Description("Expand level of object (0 by default)") + public void setExpand(Integer expand) { + this.expand = expand; + } + + @Override + public void process(CommandProcess process) { + // TODO: null check + RowAffect affect = new RowAffect(); + Instrumentation inst = process.session().getInstrumentation(); + Set> matchedClasses = SearchUtils.searchClass(inst, classPattern, isRegEx); + + for (Class clazz : matchedClasses) { + processClass(process, clazz); + } + + affect.rCnt(matchedClasses.size()); + process.write(affect + "\n"); + process.end(); + } + + private void processClass(CommandProcess process, Class clazz) { + if (isDetail) { + process.write(RenderUtil.render(renderClassInfo(clazz, isField), process.width()) + "\n"); + } else { + process.write(clazz.getName() + "\n"); + } + } + + private Element renderClassInfo(Class clazz, boolean isPrintField) { + TableElement table = new TableElement().leftCellPadding(1).rightCellPadding(1); + CodeSource cs = clazz.getProtectionDomain().getCodeSource(); + + table.row(label("class-info").style(Decoration.bold.bold()), label(StringUtils.classname(clazz))) + .row(label("code-source").style(Decoration.bold.bold()), label(getCodeSource(cs))) + .row(label("name").style(Decoration.bold.bold()), label(StringUtils.classname(clazz))) + .row(label("isInterface").style(Decoration.bold.bold()), label("" + clazz.isInterface())) + .row(label("isAnnotation").style(Decoration.bold.bold()), label("" + clazz.isAnnotation())) + .row(label("isEnum").style(Decoration.bold.bold()), label("" + clazz.isEnum())) + .row(label("isAnonymousClass").style(Decoration.bold.bold()), label("" + clazz.isAnonymousClass())) + .row(label("isArray").style(Decoration.bold.bold()), label("" + clazz.isArray())) + .row(label("isLocalClass").style(Decoration.bold.bold()), label("" + clazz.isLocalClass())) + .row(label("isMemberClass").style(Decoration.bold.bold()), label("" + clazz.isMemberClass())) + .row(label("isPrimitive").style(Decoration.bold.bold()), label("" + clazz.isPrimitive())) + .row(label("isSynthetic").style(Decoration.bold.bold()), label("" + clazz.isSynthetic())) + .row(label("simple-name").style(Decoration.bold.bold()), label(clazz.getSimpleName())) + .row(label("modifier").style(Decoration.bold.bold()), label(StringUtils.modifier(clazz.getModifiers(), ','))) + .row(label("annotation").style(Decoration.bold.bold()), label(TypeRenderUtils.drawAnnotation(clazz))) + .row(label("interfaces").style(Decoration.bold.bold()), label(TypeRenderUtils.drawInterface(clazz))) + .row(label("super-class").style(Decoration.bold.bold()), TypeRenderUtils.drawSuperClass(clazz)) + .row(label("class-loader").style(Decoration.bold.bold()), TypeRenderUtils.drawClassLoader(clazz)) + .row(label("classLoaderHash").style(Decoration.bold.bold()), label(StringUtils.classLoaderHash(clazz))); + + if (isPrintField) { + table.row(label("fields"), TypeRenderUtils.drawField(clazz, expand)); + } + return table; + } + + public static String getCodeSource(final CodeSource cs) { + if (null == cs || null == cs.getLocation() || null == cs.getLocation().getFile()) { + return com.taobao.arthas.core.util.Constants.EMPTY_STRING; + } + + return cs.getLocation().getFile(); + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/klass100/SearchMethodCommand.java b/core/src/main/java/com/taobao/arthas/core/command/klass100/SearchMethodCommand.java new file mode 100644 index 00000000..e6c9ab4f --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/klass100/SearchMethodCommand.java @@ -0,0 +1,160 @@ +package com.taobao.arthas.core.command.klass100; + +import com.taobao.arthas.core.command.Constants; +import com.taobao.arthas.core.shell.command.AnnotatedCommand; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.util.matcher.Matcher; +import com.taobao.arthas.core.util.matcher.RegexMatcher; +import com.taobao.arthas.core.util.matcher.WildcardMatcher; +import com.taobao.arthas.core.util.SearchUtils; +import com.taobao.arthas.core.util.StringUtils; +import com.taobao.arthas.core.util.TypeRenderUtils; +import com.taobao.arthas.core.util.affect.RowAffect; +import com.taobao.middleware.cli.annotations.Argument; +import com.taobao.middleware.cli.annotations.Description; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Option; +import com.taobao.middleware.cli.annotations.Summary; +import com.taobao.text.ui.Element; +import com.taobao.text.ui.TableElement; +import com.taobao.text.util.RenderUtil; + +import java.lang.instrument.Instrumentation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Set; + +import static com.taobao.text.Decoration.bold; +import static com.taobao.text.ui.Element.label; +import static java.lang.String.format; + +/** + * 展示方法信息 + * + * @author vlinux + */ +@Name("sm") +@Summary("Search the method of classes loaded by JVM") +@Description(Constants.EXAMPLE + + " sm -Ed org\\\\.apache\\\\.commons\\\\.lang\\.StringUtils .*\n" + + " sm org.apache.commons.????.StringUtils *\n" + + " sm -d org.apache.commons.lang.StringUtils\n" + + " sm -d org/apache/commons/lang/StringUtils\n" + + " sm *String????s *\n" + + Constants.WIKI + Constants.WIKI_HOME + "cmds/sm") +public class SearchMethodCommand extends AnnotatedCommand { + + private String classPattern; + private String methodPattern; + private boolean isDetail = false; + private boolean isRegEx = false; + + @Argument(argName = "class-pattern", index = 0) + @Description("Class name pattern, use either '.' or '/' as separator") + public void setClassPattern(String classPattern) { + this.classPattern = classPattern; + } + + @Argument(argName = "method-pattern", index = 1, required = false) + @Description("Method name pattern") + public void setMethodPattern(String methodPattern) { + this.methodPattern = methodPattern; + } + + @Option(shortName = "d", longName = "details", flag = true) + @Description("Display the details of method") + public void setDetail(boolean detail) { + isDetail = detail; + } + + @Option(shortName = "E", longName = "regex", flag = true) + @Description("Enable regular expression to match (wildcard matching by default)") + public void setRegEx(boolean regEx) { + isRegEx = regEx; + } + + @Override + public void process(CommandProcess process) { + RowAffect affect = new RowAffect(); + + Instrumentation inst = process.session().getInstrumentation(); + Matcher methodNameMatcher = methodNameMatcher(); + Set> matchedClasses = SearchUtils.searchClass(inst, classPattern, isRegEx); + + for (Class clazz : matchedClasses) { + Set methodNames = new HashSet(); + for (Constructor constructor : clazz.getDeclaredConstructors()) { + if (!methodNameMatcher.matching("")) { + continue; + } + + if (isDetail) { + process.write(RenderUtil.render(renderConstructor(constructor), process.width()) + "\n"); + } else { + if (methodNames.contains("")) { + continue; + } + methodNames.add(""); + String line = format("%s->%s%n", clazz.getName(), ""); + process.write(line); + } + affect.rCnt(1); + } + + for (Method method : clazz.getDeclaredMethods()) { + if (!methodNameMatcher.matching(method.getName())) { + continue; + } + + if (isDetail) { + process.write(RenderUtil.render(renderMethod(method), process.width()) + "\n"); + } else { + if (methodNames.contains(method.getName())) { + continue; + } + methodNames.add(method.getName()); + String line = format("%s->%s%n", clazz.getName(), method.getName()); + process.write(line); + } + affect.rCnt(1); + } + } + + process.write(affect + "\n"); + process.end(); + } + + private Matcher methodNameMatcher() { + // auto fix default methodPattern + if (StringUtils.isBlank(methodPattern)) { + methodPattern = isRegEx ? ".*" : "*"; + } + return isRegEx ? new RegexMatcher(methodPattern) : new WildcardMatcher(methodPattern); + } + + private Element renderMethod(Method method) { + TableElement table = new TableElement().leftCellPadding(1).rightCellPadding(1); + + table.row(label("declaring-class").style(bold.bold()), label(method.getDeclaringClass().getName())) + .row(label("method-name").style(bold.bold()), label(method.getName()).style(bold.bold())) + .row(label("modifier").style(bold.bold()), label(StringUtils.modifier(method.getModifiers(), ','))) + .row(label("annotation").style(bold.bold()), label(TypeRenderUtils.drawAnnotation(method))) + .row(label("parameters").style(bold.bold()), label(TypeRenderUtils.drawParameters(method))) + .row(label("return").style(bold.bold()), label(TypeRenderUtils.drawReturn(method))) + .row(label("exceptions").style(bold.bold()), label(TypeRenderUtils.drawExceptions(method))); + return table; + } + + private Element renderConstructor(Constructor constructor) { + TableElement table = new TableElement().leftCellPadding(1).rightCellPadding(1); + + table.row(label("declaring-class").style(bold.bold()), label(constructor.getDeclaringClass().getName())) + .row(label("constructor-name").style(bold.bold()), label("").style(bold.bold())) + .row(label("modifier").style(bold.bold()), label(StringUtils.modifier(constructor.getModifiers(), ','))) + .row(label("annotation").style(bold.bold()), label(TypeRenderUtils.drawAnnotation(constructor.getDeclaredAnnotations()))) + .row(label("parameters").style(bold.bold()), label(TypeRenderUtils.drawParameters(constructor))) + .row(label("exceptions").style(bold.bold()), label(TypeRenderUtils.drawExceptions(constructor))); + return table; + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/AbstractTraceAdviceListener.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/AbstractTraceAdviceListener.java new file mode 100644 index 00000000..a8ef9ae9 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/AbstractTraceAdviceListener.java @@ -0,0 +1,95 @@ +package com.taobao.arthas.core.command.monitor200; + +import com.taobao.arthas.core.advisor.Advice; +import com.taobao.arthas.core.advisor.ArthasMethod; +import com.taobao.arthas.core.advisor.ReflectAdviceListenerAdapter; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.util.LogUtil; +import com.taobao.arthas.core.util.ThreadLocalWatch; + +/** + * @author ralf0131 2017-01-06 16:02. + */ +public class AbstractTraceAdviceListener extends ReflectAdviceListenerAdapter { + + protected final ThreadLocalWatch threadLocalWatch = new ThreadLocalWatch(); + protected TraceCommand command; + protected CommandProcess process; + + protected final ThreadLocal threadBoundEntity = new ThreadLocal() { + + @Override + protected TraceEntity initialValue() { + return new TraceEntity(); + } + }; + + /** + * Constructor + */ + public AbstractTraceAdviceListener(TraceCommand command, CommandProcess process) { + this.command = command; + this.process = process; + } + + @Override + public void destroy() { + threadBoundEntity.remove(); + } + + @Override + public void before(ClassLoader loader, Class clazz, ArthasMethod method, Object target, Object[] args) + throws Throwable { + threadBoundEntity.get().view.begin(clazz.getName() + ":" + method.getName() + "()"); + threadBoundEntity.get().deep++; + // 开始计算本次方法调用耗时 + threadLocalWatch.start(); + } + + @Override + public void afterReturning(ClassLoader loader, Class clazz, ArthasMethod method, Object target, Object[] args, + Object returnObject) throws Throwable { + threadBoundEntity.get().view.end(); + final Advice advice = Advice.newForAfterRetuning(loader, clazz, method, target, args, returnObject); + finishing(advice); + } + + @Override + public void afterThrowing(ClassLoader loader, Class clazz, ArthasMethod method, Object target, Object[] args, + Throwable throwable) throws Throwable { + threadBoundEntity.get().view.begin("throw:" + throwable.getClass().getName() + "()").end().end(); + final Advice advice = Advice.newForAfterThrowing(loader, clazz, method, target, args, throwable); + finishing(advice); + } + + public TraceCommand getCommand() { + return command; + } + + private void finishing(Advice advice) { + // 本次调用的耗时 + double cost = threadLocalWatch.costInMillis(); + if (--threadBoundEntity.get().deep == 0) { + try { + if (isConditionMet(command.getConditionExpress(), advice, cost)) { + // 满足输出条件 + if (isLimitExceeded(command.getNumberOfLimit(), process.times().get())) { + // TODO: concurrency issue to abort process + abortProcess(process, command.getNumberOfLimit()); + } else { + process.times().incrementAndGet(); + // TODO: concurrency issues for process.write + process.write(threadBoundEntity.get().view.draw() + "\n"); + } + } + } catch (Throwable e) { + LogUtil.getArthasLogger().warn("trace failed.", e); + process.write("trace failed, condition is: " + command.getConditionExpress() + ", " + e.getMessage() + + ", visit " + LogUtil.LOGGER_FILE + " for more details.\n"); + process.end(); + } finally { + threadBoundEntity.remove(); + } + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/CompleteContext.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/CompleteContext.java new file mode 100644 index 00000000..ed923e8c --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/CompleteContext.java @@ -0,0 +1,53 @@ +package com.taobao.arthas.core.command.monitor200; + +/** + * @author ralf0131 2017-01-11 14:57. + */ +public class CompleteContext { + + private CompleteState state; + + public CompleteContext() { + this.state = CompleteState.INIT; + } + + public void setState(CompleteState state) { + this.state = state; + } + + public CompleteState getState() { + return state; + } + + /** + * The state transition diagram is: + * INIT -> CLASS_NAME -> METHOD_NAME -> FINISHED + */ + enum CompleteState { + + /** + * the state that nothing is completed + */ + INIT, + + /** + * the state that class name is completed + */ + CLASS_COMPLETED, + + /** + * the state that method name is completed + */ + METHOD_COMPLETED, + + /** + * the state that express is completed + */ + EXPRESS_COMPLETED, + + /** + * the state that condition-express is completed + */ + CONDITION_EXPRESS_COMPLETED + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/DashboardCommand.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/DashboardCommand.java new file mode 100644 index 00000000..c71cd119 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/DashboardCommand.java @@ -0,0 +1,414 @@ +package com.taobao.arthas.core.command.monitor200; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.taobao.arthas.core.command.Constants; +import com.taobao.arthas.core.shell.command.AnnotatedCommand; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.shell.handlers.Handler; +import com.taobao.arthas.core.shell.session.Session; +import com.taobao.arthas.core.util.LogUtil; +import com.taobao.arthas.core.util.NetUtils; +import com.taobao.arthas.core.util.NetUtils.Response; +import com.taobao.arthas.core.util.ThreadUtil; +import com.taobao.arthas.core.util.metrics.SumRateCounter; +import com.taobao.middleware.cli.annotations.Description; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Option; +import com.taobao.middleware.cli.annotations.Summary; +import com.taobao.middleware.logger.Logger; +import com.taobao.text.Color; +import com.taobao.text.Decoration; +import com.taobao.text.Style; +import com.taobao.text.renderers.ThreadRenderer; +import com.taobao.text.ui.RowElement; +import com.taobao.text.ui.TableElement; +import com.taobao.text.util.RenderUtil; + +import java.lang.management.BufferPoolMXBean; +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryPoolMXBean; +import java.lang.management.MemoryType; +import java.lang.management.MemoryUsage; +import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; + +/** + * @author hengyunabc 2015年11月19日 上午11:57:21 + */ +@Name("dashboard") +@Summary("Overview of target jvm's thread, memory, gc, vm, tomcat info.") +@Description(Constants.EXAMPLE + + " dashboard\n" + + " dashboard -n 10\n" + + " dashboard -i 2000\n" + + Constants.WIKI + Constants.WIKI_HOME + "cmds/dashboard") +public class DashboardCommand extends AnnotatedCommand { + + private static final Logger logger = LogUtil.getArthasLogger(); + + private SumRateCounter tomcatRequestCounter = new SumRateCounter(); + private SumRateCounter tomcatErrorCounter = new SumRateCounter(); + private SumRateCounter tomcatReceivedBytesCounter = new SumRateCounter(); + private SumRateCounter tomcatSentBytesCounter = new SumRateCounter(); + + private int numOfExecutions = Integer.MAX_VALUE; + + private boolean batchMode; + + private long interval = 5000; + + private volatile long count = 0; + private volatile Timer timer; + private Boolean running = false; + + @Option(shortName = "n", longName = "number-of-execution") + @Description("The number of times this command will be executed.") + public void setNumOfExecutions(int numOfExecutions) { + this.numOfExecutions = numOfExecutions; + } + + @Option(shortName = "b", longName = "batch") + @Description("Execute this command in batch mode.") + public void setBatchMode(boolean batchMode) { + this.batchMode = batchMode; + } + + @Option(shortName = "i", longName = "interval") + @Description("The interval (in ms) between two executions, default is 5000 ms.") + public void setInterval(long interval) { + this.interval = interval; + } + + + @Override + public void process(final CommandProcess process) { + + Session session = process.session(); + timer = new Timer("Timer-for-arthas-dashboard-" + session.getSessionId(), true); + + // ctrl-C support + process.interruptHandler(new DashboardInterruptHandler(process, timer)); + + /* + * 通过handle回调,在suspend和end时停止timer,resume时重启timer + */ + Handler stopHandler = new Handler() { + @Override + public void handle(Void event) { + stop(); + } + }; + + Handler restartHandler = new Handler() { + @Override + public void handle(Void event) { + restart(process); + } + }; + process.suspendHandler(stopHandler); + process.resumeHandler(restartHandler); + process.endHandler(stopHandler); + + // start the timer + timer.scheduleAtFixedRate(new DashboardTimerTask(process), 0, getInterval()); + running = true; + } + + public synchronized void stop() { + if (timer != null) { + timer.cancel(); + timer.purge(); + timer = null; + } + } + + public synchronized void restart(CommandProcess process) { + if (timer == null) { + Session session = process.session(); + timer = new Timer("Timer-for-arthas-dashboard-" + session.getSessionId(), true); + timer.scheduleAtFixedRate(new DashboardTimerTask(process), 0, getInterval()); + } + } + + public int getNumOfExecutions() { + return numOfExecutions; + } + + public boolean isBatchMode() { + return batchMode; + } + + public long getInterval() { + return interval; + } + + private static String beautifyName(String name) { + return name.replace(' ', '_').toLowerCase(); + } + + private static void addBufferPoolMemoryInfo(TableElement table) { + try { + @SuppressWarnings("rawtypes") + Class bufferPoolMXBeanClass = Class.forName("java.lang.management.BufferPoolMXBean"); + @SuppressWarnings("unchecked") + List bufferPoolMXBeans = ManagementFactory.getPlatformMXBeans(bufferPoolMXBeanClass); + for (BufferPoolMXBean mbean : bufferPoolMXBeans) { + long used = mbean.getMemoryUsed(); + long total = mbean.getTotalCapacity(); + new MemoryEntry(mbean.getName(), used, total, Long.MIN_VALUE).addTableRow(table); + } + } catch (ClassNotFoundException e) { + // ignore + } + } + + private static void addRuntimeInfo(TableElement table) { + table.row("os.name", System.getProperty("os.name")); + table.row("os.version", System.getProperty("os.version")); + table.row("java.version", System.getProperty("java.version")); + table.row("java.home", System.getProperty("java.home")); + table.row("systemload.average", + String.format("%.2f", ManagementFactory.getOperatingSystemMXBean().getSystemLoadAverage())); + table.row("processors", "" + Runtime.getRuntime().availableProcessors()); + table.row("uptime", "" + ManagementFactory.getRuntimeMXBean().getUptime() / 1000 + "s"); + } + + private static void addMemoryInfo(TableElement table) { + MemoryUsage heapMemoryUsage = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage(); + MemoryUsage nonHeapMemoryUsage = ManagementFactory.getMemoryMXBean().getNonHeapMemoryUsage(); + + List memoryPoolMXBeans = ManagementFactory.getMemoryPoolMXBeans(); + + new MemoryEntry("heap", heapMemoryUsage).addTableRow(table, Decoration.bold.bold()); + for (MemoryPoolMXBean poolMXBean : memoryPoolMXBeans) { + if (MemoryType.HEAP.equals(poolMXBean.getType())) { + MemoryUsage usage = poolMXBean.getUsage(); + String poolName = beautifyName(poolMXBean.getName()); + new MemoryEntry(poolName, usage).addTableRow(table); + } + } + + new MemoryEntry("nonheap", nonHeapMemoryUsage).addTableRow(table, Decoration.bold.bold()); + for (MemoryPoolMXBean poolMXBean : memoryPoolMXBeans) { + if (MemoryType.NON_HEAP.equals(poolMXBean.getType())) { + MemoryUsage usage = poolMXBean.getUsage(); + String poolName = beautifyName(poolMXBean.getName()); + new MemoryEntry(poolName, usage).addTableRow(table); + } + } + + addBufferPoolMemoryInfo(table); + } + + private static void addGcInfo(TableElement table) { + List garbageCollectorMxBeans = ManagementFactory.getGarbageCollectorMXBeans(); + for (GarbageCollectorMXBean garbageCollectorMXBean : garbageCollectorMxBeans) { + String name = garbageCollectorMXBean.getName(); + table.add(new RowElement().style(Decoration.bold.bold()).add("gc." + beautifyName(name) + ".count", + "" + garbageCollectorMXBean.getCollectionCount())); + table.row("gc." + beautifyName(name) + ".time(ms)", "" + garbageCollectorMXBean.getCollectionTime()); + } + } + + private static String formatBytes(long size) { + int unit = 1; + String unitStr = "B"; + if (size / 1024 > 0) { + unit = 1024; + unitStr = "K"; + } else if (size / 1024 / 1024 > 0) { + unit = 1024 * 1024; + unitStr = "M"; + } + + return String.format("%d%s", size / unit, unitStr); + } + + private void addTomcatInfo(TableElement table) { + + String threadPoolPath = "http://localhost:8006/connector/threadpool"; + String connectorStatPath = "http://localhost:8006/connector/stats"; + Response connectorStatResponse = NetUtils.request(connectorStatPath); + if (connectorStatResponse.isSuccess()) { + List connectorStats = JSON.parseArray(connectorStatResponse.getContent(), JSONObject.class); + for (JSONObject stat : connectorStats) { + String name = stat.getString("name").replace("\"", ""); + long bytesReceived = stat.getLongValue("bytesReceived"); + long bytesSent = stat.getLongValue("bytesSent"); + long processingTime = stat.getLongValue("processingTime"); + long requestCount = stat.getLongValue("requestCount"); + long errorCount = stat.getLongValue("errorCount"); + + tomcatRequestCounter.update(requestCount); + tomcatErrorCounter.update(errorCount); + tomcatReceivedBytesCounter.update(bytesReceived); + tomcatSentBytesCounter.update(bytesSent); + + table.add(new RowElement().style(Decoration.bold.bold()).add("connector", name)); + table.row("QPS", String.format("%.2f", tomcatRequestCounter.rate())); + table.row("RT(ms)", String.format("%.2f", processingTime / (double) requestCount)); + table.row("error/s", String.format("%.2f", tomcatErrorCounter.rate())); + table.row("received/s", formatBytes((long) tomcatReceivedBytesCounter.rate())); + table.row("sent/s", formatBytes((long) tomcatSentBytesCounter.rate())); + } + } + + Response threadPoolResponse = NetUtils.request(threadPoolPath); + if (threadPoolResponse.isSuccess()) { + List threadPoolInfos = JSON.parseArray(threadPoolResponse.getContent(), JSONObject.class); + for (JSONObject info : threadPoolInfos) { + String name = info.getString("name").replace("\"", ""); + long busy = info.getLongValue("threadBusy"); + long total = info.getLongValue("threadCount"); + table.add(new RowElement().style(Decoration.bold.bold()).add("threadpool", name)); + table.row("busy", "" + busy); + table.row("total", "" + total); + } + } + } + + static String drawThreadInfo(int width, int height) { + Map threads = ThreadUtil.getThreads(); + return RenderUtil.render(threads.values().iterator(), new ThreadRenderer(), width, height); + } + + static String drawMemoryInfoAndGcInfo(int width, int height) { + TableElement table = new TableElement(1, 1); + + TableElement memoryInfoTable = new TableElement(3, 1, 1, 1, 1).rightCellPadding(1); + memoryInfoTable.add(new RowElement().style(Decoration.bold.fg(Color.black).bg(Color.white)).add("Memory", + "used", "total", "max", "usage")); + + addMemoryInfo(memoryInfoTable); + + TableElement gcInfoTable = new TableElement(1, 1).rightCellPadding(1); + gcInfoTable.add(new RowElement().style(Decoration.bold.fg(Color.black).bg(Color.white)).add("GC", "")); + addGcInfo(gcInfoTable); + + table.row(memoryInfoTable, gcInfoTable); + return RenderUtil.render(table, width, height); + } + + String drawRuntineInfoAndTomcatInfo(int width, int height) { + TableElement table = new TableElement(1, 1); + + TableElement runtimeInfoTable = new TableElement(1, 1).rightCellPadding(1); + runtimeInfoTable + .add(new RowElement().style(Decoration.bold.fg(Color.black).bg(Color.white)).add("Runtime", "")); + + addRuntimeInfo(runtimeInfoTable); + + TableElement tomcatInfoTable = new TableElement(1, 1).rightCellPadding(1); + + try { + // 如果请求tomcat信息失败,则不显示tomcat信息 + if (NetUtils.request("http://localhost:8006").isSuccess()) { + tomcatInfoTable + .add(new RowElement().style(Decoration.bold.fg(Color.black).bg(Color.white)).add("Tomcat", "")); + addTomcatInfo(tomcatInfoTable); + } + } catch (Throwable t) { + logger.error(null, "get Tomcat Info error!", t); + } + + table.row(runtimeInfoTable, tomcatInfoTable); + return RenderUtil.render(table, width, height); + } + + static class MemoryEntry { + String name; + long used; + long total; + long max; + + int unit; + String unitStr; + + public MemoryEntry(String name, long used, long total, long max) { + this.name = name; + this.used = used; + this.total = total; + this.max = max; + + unitStr = "K"; + unit = 1024; + if (used / 1024 / 1024 > 0) { + unitStr = "M"; + unit = 1024 * 1024; + } + } + + public MemoryEntry(String name, MemoryUsage usage) { + this(name, usage.getUsed(), usage.getCommitted(), usage.getMax()); + } + + private String format(long value) { + String valueStr = "-"; + if (value == -1) { + return "-1"; + } + if (value != Long.MIN_VALUE) { + valueStr = value / unit + unitStr; + } + return valueStr; + } + + public void addTableRow(TableElement table) { + double usage = used / (double) (max == -1 || max == Long.MIN_VALUE ? total : max) * 100; + + table.row(name, format(used), format(total), format(max), String.format("%.2f%%", usage)); + } + + public void addTableRow(TableElement table, Style.Composite style) { + double usage = used / (double) (max == -1 || max == Long.MIN_VALUE ? total : max) * 100; + + table.add(new RowElement().style(style).add(name, format(used), format(total), format(max), + String.format("%.2f%%", usage))); + } + } + + private class DashboardTimerTask extends TimerTask { + private CommandProcess process; + + public DashboardTimerTask(CommandProcess process) { + this.process = process; + } + + @Override + public void run() { + if (count >= getNumOfExecutions()) { + // stop the timer + timer.cancel(); + timer.purge(); + process.write("Process ends after " + getNumOfExecutions() + " time(s).\n"); + process.end(); + return; + } + + int width = process.width(); + int height = process.height(); + + // 上半部分放thread top。下半部分再切分为田字格,其中上面两格放memory, gc的信息。下面两格放tomcat, + // runtime的信息 + int totalHeight = height - 1; + int threadTopHeight = totalHeight / 2; + int lowerHalf = totalHeight - threadTopHeight; + + int runtimeInfoHeight = lowerHalf / 2; + int heapInfoHeight = lowerHalf - runtimeInfoHeight; + + String threadInfo = drawThreadInfo(width, threadTopHeight); + String memoryAndGc = drawMemoryInfoAndGcInfo(width, runtimeInfoHeight); + String runTimeAndTomcat = drawRuntineInfoAndTomcatInfo(width, heapInfoHeight); + + process.write(threadInfo + memoryAndGc + runTimeAndTomcat); + + count++; + process.times().incrementAndGet(); + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/DashboardInterruptHandler.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/DashboardInterruptHandler.java new file mode 100644 index 00000000..317b6ac7 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/DashboardInterruptHandler.java @@ -0,0 +1,25 @@ +package com.taobao.arthas.core.command.monitor200; + +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.shell.handlers.command.CommandInterruptHandler; + +import java.util.Timer; + +/** + * @author ralf0131 2017-01-09 13:37. + */ +public class DashboardInterruptHandler extends CommandInterruptHandler { + + private volatile Timer timer; + + public DashboardInterruptHandler(CommandProcess process, Timer timer) { + super(process); + this.timer = timer; + } + + @Override + public void handle(Void event) { + timer.cancel(); + super.handle(event); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/EnhancerCommand.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/EnhancerCommand.java new file mode 100644 index 00000000..df5171cc --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/EnhancerCommand.java @@ -0,0 +1,310 @@ +package com.taobao.arthas.core.command.monitor200; + +import com.taobao.arthas.core.advisor.AdviceListener; +import com.taobao.arthas.core.advisor.Enhancer; +import com.taobao.arthas.core.advisor.InvokeTraceable; +import com.taobao.arthas.core.shell.cli.CliToken; +import com.taobao.arthas.core.shell.cli.Completion; +import com.taobao.arthas.core.shell.cli.CompletionUtils; +import com.taobao.arthas.core.shell.command.AnnotatedCommand; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.shell.handlers.command.CommandInterruptHandler; +import com.taobao.arthas.core.shell.session.Session; +import com.taobao.arthas.core.util.Constants; +import com.taobao.arthas.core.util.LogUtil; +import com.taobao.arthas.core.util.SearchUtils; +import com.taobao.arthas.core.util.affect.EnhancerAffect; +import com.taobao.arthas.core.util.matcher.Matcher; +import com.taobao.middleware.cli.CLI; +import com.taobao.middleware.cli.annotations.CLIConfigurator; +import com.taobao.middleware.logger.Logger; + +import java.lang.instrument.Instrumentation; +import java.lang.instrument.UnmodifiableClassException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +/** + * @author beiwei30 on 29/11/2016. + */ +public abstract class EnhancerCommand extends AnnotatedCommand { + + private static final Logger logger = LogUtil.getArthasLogger(); + private static final int SIZE_LIMIT = 50; + private static final int MINIMAL_COMPLETE_SIZE = 3; + protected static final List EMPTY = Collections.emptyList(); + private static final String[] EXPRESS_EXAMPLES = { "params", "returnObj", "throwExp", "target", "clazz", "method", + "{params,returnObj}", "params[0]" }; + + protected Matcher classNameMatcher; + protected Matcher methodNameMatcher; + + /** + * 类名匹配 + * + * @return 获取类名匹配 + */ + protected abstract Matcher getClassNameMatcher(); + + /** + * 方法名匹配 + * + * @return 获取方法名匹配 + */ + protected abstract Matcher getMethodNameMatcher(); + + /** + * 获取监听器 + * + * @return 返回监听器 + */ + protected abstract AdviceListener getAdviceListener(CommandProcess process); + + @Override + public void process(final CommandProcess process) { + // ctrl-C support + process.interruptHandler(new CommandInterruptHandler(process)); + + // start to enhance + enhance(process); + } + + @Override + public void complete(Completion completion) { + List tokens = completion.lineTokens(); + CliToken lastToken = tokens.get(tokens.size() - 1); + + CompleteContext completeContext = getCompleteContext(completion); + if (completeContext == null) { + completeDefault(completion, lastToken); + return; + } + + switch (completeContext.getState()) { + case INIT: + if (completeClassName(completion)) { + completeContext.setState(CompleteContext.CompleteState.CLASS_COMPLETED); + } + break; + case CLASS_COMPLETED: + if (completeMethodName(completion)) { + completeContext.setState(CompleteContext.CompleteState.METHOD_COMPLETED); + } + break; + case METHOD_COMPLETED: + if (completeExpress(completion)) { + completeContext.setState(CompleteContext.CompleteState.EXPRESS_COMPLETED); + } + break; + case EXPRESS_COMPLETED: + if (completeConditionExpress(completion)) { + completeContext.setState(CompleteContext.CompleteState.CONDITION_EXPRESS_COMPLETED); + } + break; + case CONDITION_EXPRESS_COMPLETED: + completion.complete(EMPTY); + } + } + + protected void enhance(CommandProcess process) { + Session session = process.session(); + if (!session.tryLock()) { + process.write("someone else is enhancing classes, pls. wait.\n"); + process.end(); + return; + } + int lock = session.getLock(); + try { + Instrumentation inst = session.getInstrumentation(); + AdviceListener listener = getAdviceListener(process); + if (listener == null) { + warn(process, "advice listener is null"); + return; + } + boolean skipJDKTrace = false; + if(listener instanceof AbstractTraceAdviceListener) { + skipJDKTrace = ((AbstractTraceAdviceListener) listener).getCommand().isSkipJDKTrace(); + } + + EnhancerAffect effect = Enhancer.enhance(inst, lock, listener instanceof InvokeTraceable, + skipJDKTrace, getClassNameMatcher(), getMethodNameMatcher()); + + if (effect.cCnt() == 0 || effect.mCnt() == 0) { + // no class effected + // might be method code too large + process.write("No class or method is affected, try:\n" + + "1. sm CLASS_NAME METHOD_NAME to make sure the method you are tracing actually exists (it might be in your parent class).\n" + + "2. reset CLASS_NAME and try again, your method body might be too large.\n" + + "3. visit middleware-container/arthas/issues/278 for more detail\n"); + process.end(); + return; + } + + // 这里做个补偿,如果在enhance期间,unLock被调用了,则补偿性放弃 + if (session.getLock() == lock) { + // 注册通知监听器 + process.register(lock, listener); + if (process.isForeground()) { + process.echoTips(Constants.ABORT_MSG + "\n"); + } + } + + process.write(effect + "\n"); + } catch (UnmodifiableClassException e) { + logger.error(null, "error happens when enhancing class", e); + } finally { + if (session.getLock() == lock) { + // enhance结束后解锁 + process.session().unLock(); + } + } + } + + /** + * @return true if the class name is successfully completed + */ + protected boolean completeClassName(Completion completion) { + CliToken lastToken = completion.lineTokens().get(completion.lineTokens().size() - 1); + if (lastToken.value().length() >= MINIMAL_COMPLETE_SIZE) { + // complete class name + Set> results = SearchUtils.searchClassOnly(completion.session().getInstrumentation(), + "*" + lastToken.value() + "*", SIZE_LIMIT); + if (results.size() >= SIZE_LIMIT) { + Iterator> it = results.iterator(); + List res = new ArrayList(SIZE_LIMIT); + while (it.hasNext()) { + res.add(it.next().getName()); + } + res.add("and possibly more..."); + completion.complete(res); + } else if (results.size() == 1) { + Class clazz = results.iterator().next(); + completion.complete(clazz.getName().substring(lastToken.value().length()), true); + return true; + } else { + List res = new ArrayList(results.size()); + for (Class clazz : results) { + res.add(clazz.getName()); + } + completion.complete(res); + } + } else { + // forget to call completion.complete will cause terminal to stuck. + completion.complete(Collections.singletonList("Too many classes to display, " + + "please try to input at least 3 characters to get auto complete working.")); + } + return false; + } + + protected boolean completeMethodName(Completion completion) { + List tokens = completion.lineTokens(); + CliToken lastToken = completion.lineTokens().get(tokens.size() - 1); + + // retrieve the class name + String className; + if (" ".equals(lastToken.value())) { + // tokens = { " ", "CLASS_NAME", " "} + className = tokens.get(tokens.size() - 2).value(); + } else { + // tokens = { " ", "CLASS_NAME", " ", "PARTIAL_METHOD_NAME"} + className = tokens.get(tokens.size() - 3).value(); + } + + Set> results = SearchUtils.searchClassOnly(completion.session().getInstrumentation(), className, 2); + if (results.isEmpty() || results.size() > 1) { + // no class found or multiple class found + completion.complete(EMPTY); + return false; + } + + Class clazz = results.iterator().next(); + + List res = new ArrayList(); + + for (Method method : clazz.getDeclaredMethods()) { + if (" ".equals(lastToken.value())) { + res.add(method.getName()); + } else if (method.getName().contains(lastToken.value())) { + res.add(method.getName()); + } + } + + if (res.size() == 1) { + completion.complete(res.get(0).substring(lastToken.value().length()), true); + return true; + } else { + completion.complete(res); + return false; + } + } + + protected boolean completeExpress(Completion completion) { + return CompletionUtils.complete(completion, Arrays.asList(EXPRESS_EXAMPLES)); + } + + protected boolean completeConditionExpress(Completion completion) { + completion.complete(EMPTY); + return true; + } + + protected void completeDefault(Completion completion, CliToken lastToken) { + CLI cli = CLIConfigurator.define(this.getClass()); + List options = cli.getOptions(); + if (lastToken == null || lastToken.isBlank()) { + // complete usage + CompletionUtils.completeUsage(completion, cli); + } else if (lastToken.value().startsWith("--")) { + // complete long option + CompletionUtils.completeLongOption(completion, lastToken, options); + } else if (lastToken.value().startsWith("-")) { + // complete short option + CompletionUtils.completeShortOption(completion, lastToken, options); + } else { + completion.complete(EMPTY); + } + } + + private CompleteContext getCompleteContext(Completion completion) { + CompleteContext completeContext = new CompleteContext(); + List tokens = completion.lineTokens(); + CliToken lastToken = tokens.get(tokens.size() - 1); + + if (lastToken.value().startsWith("-") || lastToken.value().startsWith("--")) { + // this is the default case + return null; + } + + int tokenCount = 0; + + for (CliToken token : tokens) { + if (" ".equals(token.value()) || token.value().startsWith("-") || token.value().startsWith("--")) { + // filter irrelevant tokens + continue; + } + tokenCount++; + } + + for (CompleteContext.CompleteState state : CompleteContext.CompleteState.values()) { + if (tokenCount == state.ordinal() || tokenCount == state.ordinal() + 1 && !" ".equals(lastToken.value())) { + completeContext.setState(state); + return completeContext; + } + } + + return completeContext; + } + + private static void warn(CommandProcess process, String message) { + logger.error(null, message); + process.write("cannot operate the current command, pls. check arthas.log\n"); + if (process.isForeground()) { + process.echoTips(Constants.ABORT_MSG + "\n"); + } + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/GroovyAdviceListener.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/GroovyAdviceListener.java new file mode 100644 index 00000000..8b97ca99 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/GroovyAdviceListener.java @@ -0,0 +1,76 @@ +package com.taobao.arthas.core.command.monitor200; + +import com.taobao.arthas.core.advisor.ReflectAdviceListenerAdapter; +import com.taobao.arthas.core.command.ScriptSupportCommand; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.advisor.Advice; +import com.taobao.arthas.core.advisor.ArthasMethod; + +/** + * Groovy support has been completed dropped in Arthas 3.0 because of severer memory leak. + * @author beiwei30 on 01/12/2016. + */ +@Deprecated +public class GroovyAdviceListener extends ReflectAdviceListenerAdapter { + private ScriptSupportCommand.ScriptListener scriptListener; + private ScriptSupportCommand.Output output; + + public GroovyAdviceListener(ScriptSupportCommand.ScriptListener scriptListener, CommandProcess process) { + this.scriptListener = scriptListener; + this.output = new CommandProcessAdaptor(process); + } + + @Override + public void create() { + scriptListener.create(output); + } + + @Override + public void destroy() { + scriptListener.destroy(output); + } + + @Override + public void before(ClassLoader loader, Class clazz, ArthasMethod method, Object target, Object[] args) + throws Throwable { + scriptListener.before(output, Advice.newForBefore(loader, clazz, method, target, args)); + } + + @Override + public void afterReturning(ClassLoader loader, Class clazz, ArthasMethod method, Object target, Object[] args, + Object returnObject) throws Throwable { + scriptListener.afterReturning(output, Advice.newForAfterRetuning(loader, clazz, method, target, args, returnObject)); + } + + @Override + public void afterThrowing(ClassLoader loader, Class clazz, ArthasMethod method, Object target, Object[] args, + Throwable throwable) throws Throwable { + scriptListener.afterThrowing(output, Advice.newForAfterThrowing(loader, clazz, method, target, args, throwable)); + } + + private static class CommandProcessAdaptor implements ScriptSupportCommand.Output { + private CommandProcess process; + + public CommandProcessAdaptor(CommandProcess process) { + this.process = process; + } + + @Override + public ScriptSupportCommand.Output print(String string) { + process.write(string); + return this; + } + + @Override + public ScriptSupportCommand.Output println(String string) { + process.write(string).write("\n"); + return this; + } + + @Override + public ScriptSupportCommand.Output finish() { + process.end(); + return this; + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/GroovyScriptCommand.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/GroovyScriptCommand.java new file mode 100644 index 00000000..2abd721e --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/GroovyScriptCommand.java @@ -0,0 +1,91 @@ +package com.taobao.arthas.core.command.monitor200; + +import com.taobao.arthas.core.advisor.AdviceListener; +import com.taobao.arthas.core.command.ScriptSupportCommand; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.util.matcher.Matcher; +import com.taobao.middleware.cli.annotations.Argument; +import com.taobao.middleware.cli.annotations.Description; +import com.taobao.middleware.cli.annotations.Hidden; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Option; +import com.taobao.middleware.cli.annotations.Summary; + +/** + * Groovy support has been completed dropped in Arthas 3.0 because of severer memory leak. + * 脚本增强命令 + * + * @author vlinux on 15/5/31. + */ +@Name("groovy") +@Hidden +@Summary("Enhanced Groovy") +@Description("Examples:\n" + + " groovy -E org\\.apache\\.commons\\.lang\\.StringUtils isBlank /tmp/watch.groovy\n" + + " groovy org.apache.commons.lang.StringUtils isBlank /tmp/watch.groovy\n" + + " groovy *StringUtils isBlank /tmp/watch.groovy\n" + + "\n" + + "WIKI:\n" + + " middleware-container/arthas/wikis/cmds/groovy") +@Deprecated +public class GroovyScriptCommand extends EnhancerCommand implements ScriptSupportCommand { + private String classPattern; + private String methodPattern; + private String scriptFilepath; + private boolean isRegEx = false; + + @Argument(index = 0, argName = "class-pattern") + @Description("Path and classname of Pattern Matching") + public void setClassPattern(String classPattern) { + this.classPattern = classPattern; + } + + @Argument(index = 1, argName = "method-pattern") + @Description("Method of Pattern Matching") + public void setMethodPattern(String methodPattern) { + this.methodPattern = methodPattern; + } + + @Argument(index = 2, argName = "script-filepath") + @Description("Filepath of Groovy script") + public void setScriptFilepath(String scriptFilepath) { + this.scriptFilepath = scriptFilepath; + } + + @Option(shortName = "E", longName = "regex") + @Description("Enable regular expression to match (wildcard matching by default)") + public void setRegEx(boolean regEx) { + isRegEx = regEx; + } + + public String getClassPattern() { + return classPattern; + } + + public String getMethodPattern() { + return methodPattern; + } + + public String getScriptFilepath() { + return scriptFilepath; + } + + public boolean isRegEx() { + return isRegEx; + } + + @Override + protected Matcher getClassNameMatcher() { + throw new UnsupportedOperationException("groovy command is not supported yet!"); + } + + @Override + protected Matcher getMethodNameMatcher() { + throw new UnsupportedOperationException("groovy command is not supported yet!"); + } + + @Override + protected AdviceListener getAdviceListener(CommandProcess process) { + throw new UnsupportedOperationException("groovy command is not supported yet!"); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/JvmCommand.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/JvmCommand.java new file mode 100644 index 00000000..beb55de3 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/JvmCommand.java @@ -0,0 +1,218 @@ +package com.taobao.arthas.core.command.monitor200; + +import com.taobao.arthas.core.command.Constants; +import com.taobao.arthas.core.shell.command.AnnotatedCommand; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.util.affect.RowAffect; +import com.taobao.middleware.cli.annotations.Description; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Summary; +import com.taobao.text.Decoration; +import com.taobao.text.ui.Element; +import com.taobao.text.ui.TableElement; +import com.taobao.text.util.RenderUtil; + +import java.lang.management.ClassLoadingMXBean; +import java.lang.management.CompilationMXBean; +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.management.MemoryManagerMXBean; +import java.lang.management.OperatingSystemMXBean; +import java.lang.management.RuntimeMXBean; +import java.lang.management.ThreadMXBean; +import java.text.SimpleDateFormat; +import java.util.Collection; +import java.util.Date; + +import static com.taobao.text.ui.Element.label; + +/** + * JVM info command + * + * @author vlinux on 15/6/6. + */ +@Name("jvm") +@Summary("Display the target JVM information") +@Description(Constants.WIKI + Constants.WIKI_HOME + "cmds/jvm") +public class JvmCommand extends AnnotatedCommand { + + private final RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean(); + private final ClassLoadingMXBean classLoadingMXBean = ManagementFactory.getClassLoadingMXBean(); + private final CompilationMXBean compilationMXBean = ManagementFactory.getCompilationMXBean(); + private final Collection garbageCollectorMXBeans = ManagementFactory.getGarbageCollectorMXBeans(); + private final Collection memoryManagerMXBeans = ManagementFactory.getMemoryManagerMXBeans(); + private final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + // private final Collection memoryPoolMXBeans = ManagementFactory.getMemoryPoolMXBeans(); + private final OperatingSystemMXBean operatingSystemMXBean = ManagementFactory.getOperatingSystemMXBean(); + private final ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); + + @Override + public void process(CommandProcess process) { + RowAffect affect = new RowAffect(); + TableElement table = new TableElement(2, 5).leftCellPadding(1).rightCellPadding(1); + table.row(true, label("RUNTIME").style(Decoration.bold.bold())); + drawRuntimeTable(table); + table.row("", ""); + table.row(true, label("CLASS-LOADING").style(Decoration.bold.bold())); + drawClassLoadingTable(table); + table.row("", ""); + table.row(true, label("COMPILATION").style(Decoration.bold.bold())); + drawCompilationTable(table); + + if (!garbageCollectorMXBeans.isEmpty()) { + table.row("", ""); + table.row(true, label("GARBAGE-COLLECTORS").style(Decoration.bold.bold())); + drawGarbageCollectorsTable(table); + } + + if (!memoryManagerMXBeans.isEmpty()) { + table.row("", ""); + table.row(true, label("MEMORY-MANAGERS").style(Decoration.bold.bold())); + drawMemoryManagersTable(table); + } + + table.row("", ""); + table.row(true, label("MEMORY").style(Decoration.bold.bold())); + drawMemoryTable(table); + table.row("", ""); + table.row(true, label("OPERATING-SYSTEM").style(Decoration.bold.bold())); + drawOperatingSystemMXBeanTable(table); + table.row("", ""); + table.row(true, label("THREAD").style(Decoration.bold.bold())); + drawThreadTable(table); + + process.write(RenderUtil.render(table, process.width())); + process.write(affect.toString()).write("\n"); + process.end(); + } + + private String toCol(Collection strings) { + final StringBuilder colSB = new StringBuilder(); + if (strings.isEmpty()) { + colSB.append("[]"); + } else { + for (String str : strings) { + colSB.append(str).append("\n"); + } + } + return colSB.toString(); + } + + private String toCol(String... stringArray) { + final StringBuilder colSB = new StringBuilder(); + if (null == stringArray + || stringArray.length == 0) { + colSB.append("[]"); + } else { + for (String str : stringArray) { + colSB.append(str).append("\n"); + } + } + return colSB.toString(); + } + + private Element drawRuntimeTable(TableElement table) { + String bootClassPath = ""; + try { + bootClassPath = runtimeMXBean.getBootClassPath(); + } catch (Exception e) { + // under jdk9 will throw UnsupportedOperationException, ignore + } + table.row("MACHINE-NAME", runtimeMXBean.getName()) + .row("JVM-START-TIME", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(runtimeMXBean.getStartTime()))) + .row("MANAGEMENT-SPEC-VERSION", runtimeMXBean.getManagementSpecVersion()) + .row("SPEC-NAME", runtimeMXBean.getSpecName()) + .row("SPEC-VENDOR", runtimeMXBean.getSpecVendor()) + .row("SPEC-VERSION", runtimeMXBean.getSpecVersion()) + .row("VM-NAME", runtimeMXBean.getVmName()) + .row("VM-VENDOR", runtimeMXBean.getVmVendor()) + .row("VM-VERSION", runtimeMXBean.getVmVersion()) + .row("INPUT-ARGUMENTS", toCol(runtimeMXBean.getInputArguments())) + .row("CLASS-PATH", runtimeMXBean.getClassPath()) + .row("BOOT-CLASS-PATH", bootClassPath) + .row("LIBRARY-PATH", runtimeMXBean.getLibraryPath()); + + return table; + } + + private Element drawClassLoadingTable(TableElement table) { + table.row("LOADED-CLASS-COUNT", "" + classLoadingMXBean.getLoadedClassCount()) + .row("TOTAL-LOADED-CLASS-COUNT", "" + classLoadingMXBean.getTotalLoadedClassCount()) + .row("UNLOADED-CLASS-COUNT", "" + classLoadingMXBean.getUnloadedClassCount()) + .row("IS-VERBOSE", "" + classLoadingMXBean.isVerbose()); + + return table; + } + + private Element drawCompilationTable(TableElement table) { + table.row("NAME", compilationMXBean.getName()); + + if (compilationMXBean.isCompilationTimeMonitoringSupported()) { + table.row("TOTAL-COMPILE-TIME", compilationMXBean.getTotalCompilationTime() + "(ms)"); + + } + return table; + } + + private Element drawGarbageCollectorsTable(TableElement table) { + for (GarbageCollectorMXBean garbageCollectorMXBean : garbageCollectorMXBeans) { + table.row(garbageCollectorMXBean.getName() + "\n[count/time]", + garbageCollectorMXBean.getCollectionCount() + "/" + garbageCollectorMXBean.getCollectionTime() + "(ms)"); + } + + return table; + } + + private Element drawMemoryManagersTable(TableElement table) { + for (final MemoryManagerMXBean memoryManagerMXBean : memoryManagerMXBeans) { + if (memoryManagerMXBean.isValid()) { + final String name = memoryManagerMXBean.isValid() + ? memoryManagerMXBean.getName() + : memoryManagerMXBean.getName() + "(Invalid)"; + + + table.row(name, toCol(memoryManagerMXBean.getMemoryPoolNames())); + } + } + + return table; + } + + private Element drawMemoryTable(TableElement table) { + table.row("HEAP-MEMORY-USAGE\n[committed/init/max/used]", + memoryMXBean.getHeapMemoryUsage().getCommitted() + + "/" + memoryMXBean.getHeapMemoryUsage().getInit() + + "/" + memoryMXBean.getHeapMemoryUsage().getMax() + + "/" + memoryMXBean.getHeapMemoryUsage().getUsed() + ); + + table.row("NO-HEAP-MEMORY-USAGE\n[committed/init/max/used]", + memoryMXBean.getNonHeapMemoryUsage().getCommitted() + + "/" + memoryMXBean.getNonHeapMemoryUsage().getInit() + + "/" + memoryMXBean.getNonHeapMemoryUsage().getMax() + + "/" + memoryMXBean.getNonHeapMemoryUsage().getUsed() + ); + + table.row("PENDING-FINALIZE-COUNT", "" + memoryMXBean.getObjectPendingFinalizationCount()); + return table; + } + + + private Element drawOperatingSystemMXBeanTable(TableElement table) { + table.row("OS", operatingSystemMXBean.getName()).row("ARCH", operatingSystemMXBean.getArch()) + .row("PROCESSORS-COUNT", "" + operatingSystemMXBean.getAvailableProcessors()) + .row("LOAD-AVERAGE", "" + operatingSystemMXBean.getSystemLoadAverage()) + .row("VERSION", operatingSystemMXBean.getVersion()); + return table; + } + + private Element drawThreadTable(TableElement table) { + table.row("COUNT", "" + threadMXBean.getThreadCount()) + .row("DAEMON-COUNT", "" + threadMXBean.getDaemonThreadCount()) + .row("LIVE-COUNT", "" + threadMXBean.getPeakThreadCount()) + .row("STARTED-COUNT", "" + threadMXBean.getTotalStartedThreadCount()); + return table; + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/MonitorAdviceListener.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/MonitorAdviceListener.java new file mode 100644 index 00000000..6184b74d --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/MonitorAdviceListener.java @@ -0,0 +1,306 @@ +package com.taobao.arthas.core.command.monitor200; + +import com.taobao.arthas.core.advisor.ReflectAdviceListenerAdapter; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.advisor.ArthasMethod; +import com.taobao.arthas.core.util.ThreadLocalWatch; +import com.taobao.text.Decoration; +import com.taobao.text.ui.TableElement; +import com.taobao.text.util.RenderUtil; + +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import static com.taobao.arthas.core.util.ArthasCheckUtils.isEquals; +import static com.taobao.text.ui.Element.label; + +/** + * 输出的内容格式为:
+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
时间戳统计周期(s)类全路径方法名调用总次数成功次数失败次数平均耗时(ms)失败率
2012-11-07 05:00:01120com.taobao.item.ItemQueryServiceImplqueryItemForDetail150010005001530%
2012-11-07 05:00:01120com.taobao.item.ItemQueryServiceImplqueryItemById900900070%
+ * + * @author beiwei30 on 28/11/2016. + */ +class MonitorAdviceListener extends ReflectAdviceListenerAdapter { + // 输出定时任务 + private Timer timer; + // 监控数据 + private ConcurrentHashMap> monitorData = new ConcurrentHashMap>(); + private final ThreadLocalWatch threadLocalWatch = new ThreadLocalWatch(); + private MonitorCommand command; + private CommandProcess process; + + MonitorAdviceListener(MonitorCommand command, CommandProcess process) { + this.command = command; + this.process = process; + } + + @Override + public synchronized void create() { + if (timer == null) { + timer = new Timer("Timer-for-arthas-monitor-" + process.session().getSessionId(), true); + timer.scheduleAtFixedRate(new MonitorTimer(monitorData, process, command.getNumberOfLimit()), + 0, command.getCycle() * 1000); + } + } + + @Override + public synchronized void destroy() { + if (null != timer) { + timer.cancel(); + timer = null; + } + } + + @Override + public void before(ClassLoader loader, Class clazz, ArthasMethod method, Object target, Object[] args) + throws Throwable { + threadLocalWatch.start(); + } + + @Override + public void afterReturning(ClassLoader loader, Class clazz, ArthasMethod method, Object target, + Object[] args, Object returnObject) throws Throwable { + finishing(clazz, method, false); + } + + @Override + public void afterThrowing(ClassLoader loader, Class clazz, ArthasMethod method, Object target, + Object[] args, Throwable throwable) { + finishing(clazz, method, true); + } + + private void finishing(Class clazz, ArthasMethod method, boolean isThrowing) { + double cost = threadLocalWatch.costInMillis(); + final Key key = new Key(clazz.getName(), method.getName()); + + while (true) { + AtomicReference value = monitorData.get(key); + if (null == value) { + monitorData.putIfAbsent(key, new AtomicReference(new Data())); + continue; + } + + while (true) { + Data oData = value.get(); + Data nData = new Data(); + nData.setCost(oData.getCost() + cost); + if (isThrowing) { + nData.setFailed(oData.getFailed() + 1); + nData.setSuccess(oData.getSuccess()); + } else { + nData.setFailed(oData.getFailed()); + nData.setSuccess(oData.getSuccess() + 1); + } + nData.setTotal(oData.getTotal() + 1); + if (value.compareAndSet(oData, nData)) { + break; + } + } + break; + } + } + + private class MonitorTimer extends TimerTask { + private Map> monitorData; + private CommandProcess process; + private int limit; + + MonitorTimer(Map> monitorData, CommandProcess process, int limit) { + this.monitorData = monitorData; + this.process = process; + this.limit = limit; + } + + @Override + public void run() { + if (monitorData.isEmpty()) { + return; + } + // 超过次数上限,则不在输出,命令终止 + if (process.times().getAndIncrement() >= limit) { + this.cancel(); + abortProcess(process, limit); + return; + } + + TableElement table = new TableElement().leftCellPadding(1).rightCellPadding(1); + table.row(true, label("timestamp").style(Decoration.bold.bold()), + label("class").style(Decoration.bold.bold()), + label("method").style(Decoration.bold.bold()), + label("total").style(Decoration.bold.bold()), + label("success").style(Decoration.bold.bold()), + label("fail").style(Decoration.bold.bold()), + label("avg-rt(ms)").style(Decoration.bold.bold()), + label("fail-rate").style(Decoration.bold.bold())); + + for (Map.Entry> entry : monitorData.entrySet()) { + final AtomicReference value = entry.getValue(); + + Data data; + while (true) { + data = value.get(); + if (value.compareAndSet(data, new Data())) { + break; + } + } + + if (null != data) { + + final DecimalFormat df = new DecimalFormat("0.00"); + + table.row( + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()), + entry.getKey().getClassName(), + entry.getKey().getMethodName(), + "" + data.getTotal(), + "" + data.getSuccess(), + "" + data.getFailed(), + df.format(div(data.getCost(), data.getTotal())), + df.format(100.0d * div(data.getFailed(), data.getTotal())) + "%" + ); + + } + } + + process.write(RenderUtil.render(table, process.width()) + "\n"); + } + + private double div(double a, double b) { + if (b == 0) { + return 0; + } + return a / b; + } + + } + + /** + * 数据监控用的Key + * + * @author vlinux + */ + private static class Key { + private final String className; + private final String methodName; + + Key(String className, String behaviorName) { + this.className = className; + this.methodName = behaviorName; + } + + public String getClassName() { + return className; + } + + public String getMethodName() { + return methodName; + } + + @Override + public int hashCode() { + return className.hashCode() + methodName.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (null == obj + || !(obj instanceof Key)) { + return false; + } + Key okey = (Key) obj; + return isEquals(okey.className, className) && isEquals(okey.methodName, methodName); + } + + } + + /** + * 数据监控用的value + * + * @author vlinux + */ + private static class Data { + private int total; + private int success; + private int failed; + private double cost; + + public int getTotal() { + return total; + } + + public void setTotal(int total) { + this.total = total; + } + + public int getSuccess() { + return success; + } + + public void setSuccess(int success) { + this.success = success; + } + + public int getFailed() { + return failed; + } + + public void setFailed(int failed) { + this.failed = failed; + } + + public double getCost() { + return cost; + } + + public void setCost(double cost) { + this.cost = cost; + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/MonitorCommand.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/MonitorCommand.java new file mode 100644 index 00000000..69a50f5a --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/MonitorCommand.java @@ -0,0 +1,128 @@ +package com.taobao.arthas.core.command.monitor200; + + +import com.taobao.arthas.core.advisor.AdviceListener; +import com.taobao.arthas.core.command.Constants; +import com.taobao.arthas.core.shell.cli.Completion; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.shell.handlers.Handler; +import com.taobao.arthas.core.util.SearchUtils; +import com.taobao.arthas.core.util.matcher.Matcher; +import com.taobao.middleware.cli.annotations.Argument; +import com.taobao.middleware.cli.annotations.Description; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Option; +import com.taobao.middleware.cli.annotations.Summary; + +/** + * 监控请求命令
+ * @author vlinux + */ +@Name("monitor") +@Summary("Monitor method execution statistics, e.g. total/success/failure count, average rt, fail rate, etc. ") +@Description("\nExamples:\n" + + " monitor org.apache.commons.lang.StringUtils isBlank\n" + + " monitor org.apache.commons.lang.StringUtils isBlank -c 5\n" + + " monitor -E org\\.apache\\.commons\\.lang\\.StringUtils isBlank\n" + + Constants.WIKI + Constants.WIKI_HOME + "cmds/monitor") +public class MonitorCommand extends EnhancerCommand { + + private String classPattern; + private String methodPattern; + private int cycle = 60; + private boolean isRegEx = false; + private int numberOfLimit = 100; + + @Argument(argName = "class-pattern", index = 0) + @Description("Path and classname of Pattern Matching") + public void setClassPattern(String classPattern) { + this.classPattern = classPattern; + } + + @Argument(argName = "method-pattern", index = 1) + @Description("Method of Pattern Matching") + public void setMethodPattern(String methodPattern) { + this.methodPattern = methodPattern; + } + + @Option(shortName = "c", longName = "cycle") + @Description("The monitor interval (in seconds), 60 seconds by default") + public void setCycle(int cycle) { + this.cycle = cycle; + } + + @Option(shortName = "E", longName = "regex") + @Description("Enable regular expression to match (wildcard matching by default)") + public void setRegEx(boolean regEx) { + isRegEx = regEx; + } + + @Option(shortName = "n", longName = "limits") + @Description("Threshold of execution times") + public void setNumberOfLimit(int numberOfLimit) { + this.numberOfLimit = numberOfLimit; + } + + public String getClassPattern() { + return classPattern; + } + + public String getMethodPattern() { + return methodPattern; + } + + public int getCycle() { + return cycle; + } + + public boolean isRegEx() { + return isRegEx; + } + + public int getNumberOfLimit() { + return numberOfLimit; + } + + @Override + protected Matcher getClassNameMatcher() { + if (classNameMatcher == null) { + classNameMatcher = SearchUtils.classNameMatcher(getClassPattern(), isRegEx()); + } + return classNameMatcher; + } + + @Override + protected Matcher getMethodNameMatcher() { + if (methodNameMatcher == null) { + methodNameMatcher = SearchUtils.classNameMatcher(getMethodPattern(), isRegEx()); + } + return methodNameMatcher; + } + + @Override + protected AdviceListener getAdviceListener(CommandProcess process) { + final AdviceListener listener = new MonitorAdviceListener(this, process); + /* + * 通过handle回调,在suspend时停止timer,resume时重启timer + */ + process.suspendHandler(new Handler() { + @Override + public void handle(Void event) { + listener.destroy(); + } + }); + process.resumeHandler(new Handler() { + @Override + public void handle(Void event) { + listener.create(); + } + }); + return listener; + } + + @Override + protected boolean completeExpress(Completion completion) { + completion.complete(EMPTY); + return true; + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/PathTraceAdviceListener.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/PathTraceAdviceListener.java new file mode 100644 index 00000000..06725d45 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/PathTraceAdviceListener.java @@ -0,0 +1,13 @@ +package com.taobao.arthas.core.command.monitor200; + +import com.taobao.arthas.core.shell.command.CommandProcess; + +/** + * @author ralf0131 2017-01-05 13:59. + */ +public class PathTraceAdviceListener extends AbstractTraceAdviceListener { + + public PathTraceAdviceListener(TraceCommand command, CommandProcess process) { + super(command, process); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/StackAdviceListener.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/StackAdviceListener.java new file mode 100644 index 00000000..a6436d3f --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/StackAdviceListener.java @@ -0,0 +1,73 @@ +package com.taobao.arthas.core.command.monitor200; + +import com.taobao.arthas.core.advisor.ReflectAdviceListenerAdapter; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.advisor.Advice; +import com.taobao.arthas.core.advisor.ArthasMethod; +import com.taobao.arthas.core.util.DateUtils; +import com.taobao.arthas.core.util.LogUtil; +import com.taobao.arthas.core.util.ThreadLocalWatch; +import com.taobao.arthas.core.util.ThreadUtil; +import com.taobao.middleware.logger.Logger; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * @author beiwei30 on 29/11/2016. + */ +public class StackAdviceListener extends ReflectAdviceListenerAdapter { + private static final Logger logger = LogUtil.getArthasLogger(); + + private final ThreadLocal stackThreadLocal = new ThreadLocal(); + private final ThreadLocalWatch threadLocalWatch = new ThreadLocalWatch(); + private StackCommand command; + private CommandProcess process; + + public StackAdviceListener(StackCommand command, CommandProcess process) { + this.command = command; + this.process = process; + } + + @Override + public void before(ClassLoader loader, Class clazz, ArthasMethod method, Object target, Object[] args) + throws Throwable { + stackThreadLocal.set(ThreadUtil.getThreadStack(Thread.currentThread())); + // 开始计算本次方法调用耗时 + threadLocalWatch.start(); + } + + @Override + public void afterThrowing(ClassLoader loader, Class clazz, ArthasMethod method, Object target, Object[] args, + Throwable throwable) throws Throwable { + Advice advice = Advice.newForAfterThrowing(loader, clazz, method, target, args, throwable); + finishing(advice); + } + + @Override + public void afterReturning(ClassLoader loader, Class clazz, ArthasMethod method, Object target, Object[] args, + Object returnObject) throws Throwable { + Advice advice = Advice.newForAfterRetuning(loader, clazz, method, target, args, returnObject); + finishing(advice); + } + + private void finishing(Advice advice) { + // 本次调用的耗时 + try { + double cost = threadLocalWatch.costInMillis(); + if (isConditionMet(command.getConditionExpress(), advice, cost)) { + // TODO: concurrency issues for process.write + process.write("ts=" + DateUtils.getCurrentDate() + ";" + stackThreadLocal.get() + "\n"); + process.times().incrementAndGet(); + if (isLimitExceeded(command.getNumberOfLimit(), process.times().get())) { + abortProcess(process, command.getNumberOfLimit()); + } + } + } catch (Exception e) { + logger.warn("stack failed.", e); + process.write("stack failed, condition is: " + command.getConditionExpress() + ", " + e.getMessage() + + ", visit " + LogUtil.LOGGER_FILE + " for more details.\n"); + process.end(); + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/StackCommand.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/StackCommand.java new file mode 100644 index 00000000..66c0f265 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/StackCommand.java @@ -0,0 +1,114 @@ +package com.taobao.arthas.core.command.monitor200; + +import com.taobao.arthas.core.advisor.AdviceListener; +import com.taobao.arthas.core.command.Constants; +import com.taobao.arthas.core.shell.cli.Completion; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.util.SearchUtils; +import com.taobao.arthas.core.util.matcher.Matcher; +import com.taobao.middleware.cli.annotations.Argument; +import com.taobao.middleware.cli.annotations.Description; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Option; +import com.taobao.middleware.cli.annotations.Summary; + +/** + * Jstack命令
+ * 负责输出当前方法执行上下文 + * + * @author vlinux + * @author hengyunabc 2016-10-31 + */ +@Name("stack") +@Summary("Display the stack trace for the specified class and method") +@Description(Constants.EXPRESS_DESCRIPTION + Constants.EXAMPLE + + " stack -E org\\.apache\\.commons\\.lang\\.StringUtils isBlank\n" + + " stack org.apache.commons.lang.StringUtils isBlank\n" + + " stack *StringUtils isBlank\n" + + " stack *StringUtils isBlank params[0].length==1\n" + + " stack *StringUtils isBlank '#cost>100'\n" + + Constants.WIKI + Constants.WIKI_HOME + "cmds/stack") +public class StackCommand extends EnhancerCommand { + private String classPattern; + private String methodPattern; + private String conditionExpress; + private boolean isRegEx = false; + private int numberOfLimit = 100; + + @Argument(index = 0, argName = "class-pattern") + @Description("Path and classname of Pattern Matching") + public void setClassPattern(String classPattern) { + this.classPattern = classPattern; + } + + @Argument(index = 1, argName = "method-pattern", required = false) + @Description("Method of Pattern Matching") + public void setMethodPattern(String methodPattern) { + this.methodPattern = methodPattern; + } + + @Argument(index = 2, argName = "condition-express", required = false) + @Description(Constants.CONDITION_EXPRESS) + public void setConditionExpress(String conditionExpress) { + this.conditionExpress = conditionExpress; + } + + @Option(shortName = "E", longName = "regex", flag = true) + @Description("Enable regular expression to match (wildcard matching by default)") + public void setRegEx(boolean regEx) { + isRegEx = regEx; + } + + @Option(shortName = "n", longName = "limits") + @Description("Threshold of execution times") + public void setNumberOfLimit(int numberOfLimit) { + this.numberOfLimit = numberOfLimit; + } + + public String getClassPattern() { + return classPattern; + } + + public String getMethodPattern() { + return methodPattern; + } + + public String getConditionExpress() { + return conditionExpress; + } + + public boolean isRegEx() { + return isRegEx; + } + + public int getNumberOfLimit() { + return numberOfLimit; + } + + @Override + protected Matcher getClassNameMatcher() { + if (classNameMatcher == null) { + classNameMatcher = SearchUtils.classNameMatcher(getClassPattern(), isRegEx()); + } + return classNameMatcher; + } + + @Override + protected Matcher getMethodNameMatcher() { + if (methodNameMatcher == null) { + methodNameMatcher = SearchUtils.classNameMatcher(getMethodPattern(), isRegEx()); + } + return methodNameMatcher; + } + + @Override + protected AdviceListener getAdviceListener(CommandProcess process) { + return new StackAdviceListener(this, process); + } + + @Override + protected boolean completeExpress(Completion completion) { + completion.complete(EMPTY); + return true; + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/ThreadCommand.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/ThreadCommand.java new file mode 100644 index 00000000..336f97d4 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/ThreadCommand.java @@ -0,0 +1,155 @@ +package com.taobao.arthas.core.command.monitor200; + +import com.taobao.arthas.core.util.ArrayUtils; +import com.taobao.arthas.core.command.Constants; +import com.taobao.arthas.core.shell.command.AnnotatedCommand; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.util.ThreadUtil; +import com.taobao.arthas.core.util.affect.Affect; +import com.taobao.arthas.core.util.affect.RowAffect; +import com.taobao.middleware.cli.annotations.Argument; +import com.taobao.middleware.cli.annotations.Description; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Option; +import com.taobao.middleware.cli.annotations.Summary; +import com.taobao.text.renderers.ThreadRenderer; +import com.taobao.text.ui.LabelElement; +import com.taobao.text.util.RenderUtil; + +import java.lang.Thread.State; +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadInfo; +import java.lang.management.ThreadMXBean; +import java.util.HashMap; +import java.util.Map; + +/** + * @author hengyunabc 2015年12月7日 下午2:06:21 + */ +@Name("thread") +@Summary("Display thread info, thread stack") +@Description(Constants.EXAMPLE + + " thread\n" + + " thread 51\n" + + " thread -n -1\n" + + " thread -n 5\n" + + " thread -b\n" + + " thread -i 2000\n" + + Constants.WIKI + Constants.WIKI_HOME + "cmds/thread") +public class ThreadCommand extends AnnotatedCommand { + + private static ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); + + private long id = -1; + private Integer topNBusy = null; + private boolean findMostBlockingThread = false; + private int sampleInterval = 100; + + @Argument(index = 0, required = false, argName = "id") + @Description("Show thread stack") + public void setId(long id) { + this.id = id; + } + + @Option(shortName = "n", longName = "top-n-threads") + @Description("The number of thread(s) to show, ordered by cpu utilization, -1 to show all.") + public void setTopNBusy(Integer topNBusy) { + this.topNBusy = topNBusy; + } + + @Option(shortName = "b", longName = "include-blocking-thread", flag = true) + @Description("Find the thread who is holding a lock that blocks the most number of threads.") + public void setFindMostBlockingThread(boolean findMostBlockingThread) { + this.findMostBlockingThread = findMostBlockingThread; + } + + @Option(shortName = "i", longName = "sample-interval") + @Description("Specify the sampling interval (in ms) when calculating cpu usage.") + public void setSampleInterval(int sampleInterval) { + this.sampleInterval = sampleInterval; + } + + @Override + public void process(CommandProcess process) { + Affect affect = new RowAffect(); + try { + if (id > 0) { + processThread(process); + } else if (topNBusy != null) { + processTopBusyThreads(process); + } else if (findMostBlockingThread) { + processBlockingThread(process); + } else { + processAllThreads(process); + } + } finally { + process.write(affect + "\n"); + process.end(); + } + } + + private void processAllThreads(CommandProcess process) { + Map threads = ThreadUtil.getThreads(); + + // 统计各种线程状态 + StringBuilder threadStat = new StringBuilder(); + Map stateCountMap = new HashMap(); + for (State s : State.values()) { + stateCountMap.put(s, 0); + } + + for (Thread thread : threads.values()) { + State threadState = thread.getState(); + Integer count = stateCountMap.get(threadState); + stateCountMap.put(threadState, count + 1); + } + + threadStat.append("Threads Total: ").append(threads.values().size()); + for (State s : State.values()) { + Integer count = stateCountMap.get(s); + threadStat.append(", ").append(s.name()).append(": ").append(count); + } + + String stat = RenderUtil.render(new LabelElement(threadStat), process.width()); + String content = RenderUtil.render(threads.values().iterator(), + new ThreadRenderer(sampleInterval), process.width()); + process.write(stat + content); + } + + private void processBlockingThread(CommandProcess process) { + ThreadUtil.BlockingLockInfo blockingLockInfo = ThreadUtil.findMostBlockingLock(); + + if (blockingLockInfo.threadInfo == null) { + process.write("No most blocking thread found!\n"); + } else { + String stacktrace = ThreadUtil.getFullStacktrace(blockingLockInfo); + process.write(stacktrace); + } + } + + private void processTopBusyThreads(CommandProcess process) { + Map topNThreads = ThreadUtil.getTopNThreads(sampleInterval, topNBusy); + Long[] tids = topNThreads.keySet().toArray(new Long[topNThreads.keySet().size()]); + ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(ArrayUtils.toPrimitive(tids), true, true); + if (threadInfos == null) { + process.write("thread do not exist! id: " + id + "\n"); + } else { + for (ThreadInfo info : threadInfos) { + String stacktrace = ThreadUtil.getFullStacktrace(info, topNThreads.get(info.getThreadId())); + process.write(stacktrace + "\n"); + } + } + } + + private void processThread(CommandProcess process) { + String content; + ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(new long[]{id}, true, true); + if (threadInfos == null || threadInfos[0] == null) { + content = "thread do not exist! id: " + id + "\n"; + } else { + // no cpu usage info + content = ThreadUtil.getFullStacktrace(threadInfos[0], -1); + } + process.write(content); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/TimeFragment.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/TimeFragment.java new file mode 100644 index 00000000..85da00c5 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/TimeFragment.java @@ -0,0 +1,33 @@ +package com.taobao.arthas.core.command.monitor200; + +import com.taobao.arthas.core.advisor.Advice; + +import java.util.Date; + +/** + * 时间碎片 + */ +class TimeFragment { + + public TimeFragment(Advice advice, Date gmtCreate, double cost) { + this.advice = advice; + this.gmtCreate = gmtCreate; + this.cost = cost; + } + + private final Advice advice; + private final Date gmtCreate; + private final double cost; + + public Advice getAdvice() { + return advice; + } + + public Date getGmtCreate() { + return gmtCreate; + } + + public double getCost() { + return cost; + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/TimeTunnelAdviceListener.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/TimeTunnelAdviceListener.java new file mode 100644 index 00000000..f31349b8 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/TimeTunnelAdviceListener.java @@ -0,0 +1,97 @@ +package com.taobao.arthas.core.command.monitor200; + +import com.taobao.arthas.core.advisor.ReflectAdviceListenerAdapter; +import com.taobao.arthas.core.command.express.ExpressException; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.advisor.Advice; +import com.taobao.arthas.core.advisor.ArthasMethod; +import com.taobao.arthas.core.util.LogUtil; +import com.taobao.arthas.core.util.ThreadLocalWatch; +import com.taobao.text.ui.TableElement; +import com.taobao.text.util.RenderUtil; + +import java.util.Date; + +import static com.taobao.arthas.core.command.monitor200.TimeTunnelTable.createTable; +import static com.taobao.arthas.core.command.monitor200.TimeTunnelTable.fillTableHeader; +import static com.taobao.arthas.core.command.monitor200.TimeTunnelTable.fillTableRow; + +/** + * @author beiwei30 on 30/11/2016. + */ +public class TimeTunnelAdviceListener extends ReflectAdviceListenerAdapter { + + private TimeTunnelCommand command; + private CommandProcess process; + + // 第一次启动标记 + private volatile boolean isFirst = true; + + // 方法执行时间戳 + private final ThreadLocalWatch threadLocalWatch = new ThreadLocalWatch(); + + public TimeTunnelAdviceListener(TimeTunnelCommand command, CommandProcess process) { + this.command = command; + this.process = process; + } + + @Override + public void before(ClassLoader loader, Class clazz, ArthasMethod method, Object target, Object[] args) + throws Throwable { + threadLocalWatch.start(); + } + + @Override + public void afterReturning(ClassLoader loader, Class clazz, ArthasMethod method, Object target, Object[] args, + Object returnObject) throws Throwable { + afterFinishing(Advice.newForAfterRetuning(loader, clazz, method, target, args, returnObject)); + } + + @Override + public void afterThrowing(ClassLoader loader, Class clazz, ArthasMethod method, Object target, Object[] args, + Throwable throwable) { + afterFinishing(Advice.newForAfterThrowing(loader, clazz, method, target, args, throwable)); + } + + private void afterFinishing(Advice advice) { + double cost = threadLocalWatch.costInMillis(); + TimeFragment timeTunnel = new TimeFragment(advice, new Date(), cost); + + // reset the timestamp + threadLocalWatch.clear(); + + boolean match = false; + try { + match = isConditionMet(command.getConditionExpress(), advice, cost); + } catch (ExpressException e) { + LogUtil.getArthasLogger().warn("tt failed.", e); + process.write("tt failed, condition is: " + command.getConditionExpress() + ", " + e.getMessage() + + ", visit " + LogUtil.LOGGER_FILE + " for more details.\n"); + process.end(); + } + + if (!match) { + return; + } + + int index = command.putTimeTunnel(timeTunnel); + TableElement table = createTable(); + + if (isFirst) { + isFirst = false; + + // 填充表格头部 + fillTableHeader(table); + } + + // 填充表格内容 + fillTableRow(table, index, timeTunnel); + + // TODO: concurrency issues for process.write + process.write(RenderUtil.render(table, process.width())); + process.times().incrementAndGet(); + if (isLimitExceeded(command.getNumberOfLimit(), process.times().get())) { + abortProcess(process, command.getNumberOfLimit()); + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/TimeTunnelCommand.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/TimeTunnelCommand.java new file mode 100644 index 00000000..b2fd47e3 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/TimeTunnelCommand.java @@ -0,0 +1,460 @@ +package com.taobao.arthas.core.command.monitor200; + +import com.taobao.arthas.core.advisor.AdviceListener; +import com.taobao.arthas.core.command.Constants; +import com.taobao.arthas.core.command.express.ExpressException; +import com.taobao.arthas.core.command.express.ExpressFactory; +import com.taobao.arthas.core.shell.cli.Completion; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.advisor.Advice; +import com.taobao.arthas.core.advisor.ArthasMethod; +import com.taobao.arthas.core.shell.handlers.command.CommandInterruptHandler; +import com.taobao.arthas.core.util.LogUtil; +import com.taobao.arthas.core.util.SearchUtils; +import com.taobao.arthas.core.util.StringUtils; +import com.taobao.arthas.core.util.affect.RowAffect; +import com.taobao.arthas.core.util.matcher.Matcher; +import com.taobao.arthas.core.view.ObjectView; +import com.taobao.middleware.cli.annotations.Argument; +import com.taobao.middleware.cli.annotations.Description; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Option; +import com.taobao.middleware.cli.annotations.Summary; +import com.taobao.text.ui.TableElement; +import com.taobao.text.util.RenderUtil; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import static java.lang.Integer.toHexString; +import static java.lang.String.format; + +/** + * 时光隧道命令
+ * 参数w/d依赖于参数i所传递的记录编号
+ * + * @author vlinux on 14/11/15. + */ +@Name("tt") +@Summary("Time Tunnel") +@Description(Constants.EXPRESS_DESCRIPTION + Constants.EXAMPLE + + " tt -t *StringUtils isEmpty\n" + + " tt -t *StringUtils isEmpty params[0].length==1\n" + + " tt -l\n" + + " tt -D\n" + + " tt -i 1000 -w params[0]\n" + + " tt -i 1000 -d\n" + + " tt -i 1000\n" + + Constants.WIKI + Constants.WIKI_HOME + "cmds/tt") +public class TimeTunnelCommand extends EnhancerCommand { + // 时间隧道(时间碎片的集合) + private static final Map timeFragmentMap = new LinkedHashMap(); + // 时间碎片序列生成器 + private static final AtomicInteger sequence = new AtomicInteger(1000); + // TimeTunnel the method call + private boolean isTimeTunnel = false; + private String classPattern; + private String methodPattern; + private String conditionExpress; + // list the TimeTunnel + private boolean isList = false; + private boolean isDeleteAll = false; + // index of TimeTunnel + private Integer index; + // expand of TimeTunnel + private Integer expand = 1; + // upper size limit + private Integer sizeLimit = 10 * 1024 * 1024; + // watch the index TimeTunnel + private String watchExpress = com.taobao.arthas.core.util.Constants.EMPTY_STRING; + private String searchExpress = com.taobao.arthas.core.util.Constants.EMPTY_STRING; + // play the index TimeTunnel + private boolean isPlay = false; + // delete the index TimeTunnel + private boolean isDelete = false; + private boolean isRegEx = false; + private int numberOfLimit = 100; + + @Argument(index = 0, argName = "class-pattern", required = false) + @Description("Path and classname of Pattern Matching") + public void setClassPattern(String classPattern) { + this.classPattern = classPattern; + } + + @Argument(index = 1, argName = "method-pattern", required = false) + @Description("Method of Pattern Matching") + public void setMethodPattern(String methodPattern) { + this.methodPattern = methodPattern; + } + + @Argument(index = 2, argName = "condition-express", required = false) + @Description(Constants.CONDITION_EXPRESS) + public void setConditionExpress(String conditionExpress) { + this.conditionExpress = conditionExpress; + } + + @Option(shortName = "t", longName = "time-tunnel", flag = true) + @Description("Record the method invocation within time fragments") + public void setTimeTunnel(boolean timeTunnel) { + isTimeTunnel = timeTunnel; + } + + @Option(shortName = "l", longName = "list", flag = true) + @Description("List all the time fragments") + public void setList(boolean list) { + isList = list; + } + + @Option(shortName = "D", longName = "delete-all", flag = true) + @Description("Delete all the time fragments") + public void setDeleteAll(boolean deleteAll) { + isDeleteAll = deleteAll; + } + + @Option(shortName = "i", longName = "index") + @Description("Display the detailed information from specified time fragment") + public void setIndex(Integer index) { + this.index = index; + } + + @Option(shortName = "x", longName = "expand") + @Description("Expand level of object (1 by default)") + public void setExpand(Integer expand) { + this.expand = expand; + } + + @Option(shortName = "M", longName = "sizeLimit") + @Description("Upper size limit in bytes for the result (10 * 1024 * 1024 by default)") + public void setSizeLimit(Integer sizeLimit) { + this.sizeLimit = sizeLimit; + } + + @Option(shortName = "w", longName = "watch-express") + @Description(value = "watch the time fragment by ognl express.\n" + Constants.EXPRESS_EXAMPLES) + public void setWatchExpress(String watchExpress) { + this.watchExpress = watchExpress; + } + + @Option(shortName = "s", longName = "search-express") + @Description("Search-expression, to search the time fragments by ognl express.\n" + + "The structure of 'advice' like conditional expression") + public void setSearchExpress(String searchExpress) { + this.searchExpress = searchExpress; + } + + @Option(shortName = "p", longName = "play", flag = true) + @Description("Replay the time fragment specified by index") + public void setPlay(boolean play) { + isPlay = play; + } + + @Option(shortName = "d", longName = "delete", flag = true) + @Description("Delete time fragment specified by index") + public void setDelete(boolean delete) { + isDelete = delete; + } + + @Option(shortName = "E", longName = "regex", flag = true) + @Description("Enable regular expression to match (wildcard matching by default)") + public void setRegEx(boolean regEx) { + isRegEx = regEx; + } + + @Option(shortName = "n", longName = "limits") + @Description("Threshold of execution times") + public void setNumberOfLimit(int numberOfLimit) { + this.numberOfLimit = numberOfLimit; + } + + public boolean isRegEx() { + return isRegEx; + } + + public String getMethodPattern() { + return methodPattern; + } + + public String getClassPattern() { + return classPattern; + } + + public String getConditionExpress() { + return conditionExpress; + } + + public int getNumberOfLimit() { + return numberOfLimit; + } + + + private boolean hasWatchExpress() { + return !StringUtils.isEmpty(watchExpress); + } + + private boolean hasSearchExpress() { + return !StringUtils.isEmpty(searchExpress); + } + + private boolean isNeedExpand() { + return null != expand && expand > 0; + } + + /** + * 检查参数是否合法 + */ + private void checkArguments() { + // 检查d/p参数是否有i参数配套 + if ((isDelete || isPlay) && null == index) { + throw new IllegalArgumentException("Time fragment index is expected, please type -i to specify"); + } + + // 在t参数下class-pattern,method-pattern + if (isTimeTunnel) { + if (StringUtils.isEmpty(classPattern)) { + throw new IllegalArgumentException("Class-pattern is expected, please type the wildcard expression to match"); + } + if (StringUtils.isEmpty(methodPattern)) { + throw new IllegalArgumentException("Method-pattern is expected, please type the wildcard expression to match"); + } + } + + // 一个参数都没有是不行滴 + if (null == index && !isTimeTunnel && !isDeleteAll && StringUtils.isEmpty(watchExpress) + && !isList && StringUtils.isEmpty(searchExpress)) { + throw new IllegalArgumentException("Argument(s) is/are expected, type 'help tt' to read usage"); + } + } + + /* + * 记录时间片段 + */ + int putTimeTunnel(TimeFragment tt) { + int indexOfSeq = sequence.getAndIncrement(); + timeFragmentMap.put(indexOfSeq, tt); + return indexOfSeq; + } + + @Override + public void process(final CommandProcess process) { + // 检查参数 + checkArguments(); + + // ctrl-C support + process.interruptHandler(new CommandInterruptHandler(process)); + + if (isTimeTunnel) { + enhance(process); + } else if (isPlay) { + processPlay(process); + } else if (isList) { + processList(process); + } else if (isDeleteAll) { + processDeleteAll(process); + } else if (isDelete) { + processDelete(process); + } else if (hasSearchExpress()) { + processSearch(process); + } else if (index != null) { + if (hasWatchExpress()) { + processWatch(process); + } else { + processShow(process); + } + } + } + + @Override + protected Matcher getClassNameMatcher() { + if (classNameMatcher == null) { + classNameMatcher = SearchUtils.classNameMatcher(getClassPattern(), isRegEx()); + } + return classNameMatcher; + } + + @Override + protected Matcher getMethodNameMatcher() { + if (methodNameMatcher == null) { + methodNameMatcher = SearchUtils.classNameMatcher(getMethodPattern(), isRegEx()); + } + return methodNameMatcher; + } + + @Override + protected AdviceListener getAdviceListener(CommandProcess process) { + return new TimeTunnelAdviceListener(this, process); + } + + @Override + protected boolean completeExpress(Completion completion) { + completion.complete(EMPTY); + return true; + } + + // 展示指定记录 + private void processShow(CommandProcess process) { + RowAffect affect = new RowAffect(); + try { + TimeFragment tf = timeFragmentMap.get(index); + if (null == tf) { + process.write(format("Time fragment[%d] does not exist.", index)).write("\n"); + return; + } + + Advice advice = tf.getAdvice(); + String className = advice.getClazz().getName(); + String methodName = advice.getMethod().getName(); + String objectAddress = advice.getTarget() == null ? "NULL" : "0x" + toHexString(advice.getTarget().hashCode()); + + TableElement table = TimeTunnelTable.createDefaultTable(); + TimeTunnelTable.drawTimeTunnel(tf, index, table); + TimeTunnelTable.drawMethod(advice, className, methodName, objectAddress, table); + TimeTunnelTable.drawParameters(advice, table, isNeedExpand(), expand); + TimeTunnelTable.drawReturnObj(advice, table, isNeedExpand(), expand, sizeLimit); + TimeTunnelTable.drawThrowException(advice, table, isNeedExpand(), expand); + + process.write(RenderUtil.render(table, process.width())); + affect.rCnt(1); + } finally { + process.write(affect.toString()).write("\n"); + process.end(); + } + } + + // 查看记录信息 + private void processWatch(CommandProcess process) { + RowAffect affect = new RowAffect(); + try { + final TimeFragment tf = timeFragmentMap.get(index); + if (null == tf) { + process.write(format("Time fragment[%d] does not exist.", index)).write("\n"); + return; + } + + Advice advice = tf.getAdvice(); + Object value = ExpressFactory.newExpress(advice).get(watchExpress); + if (isNeedExpand()) { + process.write(new ObjectView(value, expand, sizeLimit).draw()).write("\n"); + } else { + process.write(StringUtils.objectToString(value)).write("\n"); + } + + affect.rCnt(1); + } catch (ExpressException e) { + LogUtil.getArthasLogger().warn("tt failed.", e); + process.write(e.getMessage() + ", visit " + LogUtil.LOGGER_FILE + " for more detail\n"); + } finally { + process.write(affect.toString()).write("\n"); + process.end(); + } + } + + // do search timeFragmentMap + private void processSearch(CommandProcess process) { + RowAffect affect = new RowAffect(); + try { + // 匹配的时间片段 + Map matchingTimeSegmentMap = new LinkedHashMap(); + for (Map.Entry entry : timeFragmentMap.entrySet()) { + int index = entry.getKey(); + TimeFragment tf = entry.getValue(); + Advice advice = tf.getAdvice(); + + // 搜索出匹配的时间片段 + if ((ExpressFactory.newExpress(advice)).is(searchExpress)) { + matchingTimeSegmentMap.put(index, tf); + } + } + + if (hasWatchExpress()) { + // 执行watchExpress + TableElement table = TimeTunnelTable.createDefaultTable(); + TimeTunnelTable.drawWatchTableHeader(table); + TimeTunnelTable.drawWatchExpress(matchingTimeSegmentMap, table, watchExpress, isNeedExpand(), expand, sizeLimit); + process.write(RenderUtil.render(table, process.width())); + } else { + // 单纯的列表格 + process.write(RenderUtil.render(TimeTunnelTable.drawTimeTunnelTable(matchingTimeSegmentMap), process.width())); + } + + affect.rCnt(matchingTimeSegmentMap.size()); + } catch (ExpressException e) { + LogUtil.getArthasLogger().warn("tt failed.", e); + process.write(e.getMessage() + ", visit " + LogUtil.LOGGER_FILE + " for more detail\n"); + } finally { + process.write(affect.toString()).write("\n"); + process.end(); + } + } + + // 删除指定记录 + private void processDelete(CommandProcess process) { + RowAffect affect = new RowAffect(); + if (timeFragmentMap.remove(index) != null) { + affect.rCnt(1); + } + process.write(format("Time fragment[%d] successfully deleted.", index)).write("\n"); + process.write(affect.toString()).write("\n"); + process.end(); + } + + private void processDeleteAll(CommandProcess process) { + int count = timeFragmentMap.size(); + RowAffect affect = new RowAffect(count); + timeFragmentMap.clear(); + process.write("Time fragments are cleaned.\n"); + process.write(affect.toString()).write("\n"); + process.end(); + } + + private void processList(CommandProcess process) { + RowAffect affect = new RowAffect(); + process.write(RenderUtil.render(TimeTunnelTable.drawTimeTunnelTable(timeFragmentMap), process.width())); + affect.rCnt(timeFragmentMap.size()); + process.write(affect.toString()).write("\n"); + process.end(); + } + + // 重放指定记录 + private void processPlay(CommandProcess process) { + RowAffect affect = new RowAffect(); + try { + TimeFragment tf = timeFragmentMap.get(index); + if (null == tf) { + process.write(format("Time fragment[%d] does not exist.", index) + "\n"); + process.write(affect + "\n"); + process.end(); + return; + } + + Advice advice = tf.getAdvice(); + String className = advice.getClazz().getName(); + String methodName = advice.getMethod().getName(); + String objectAddress = advice.getTarget() == null ? "NULL" : "0x" + toHexString(advice.getTarget().hashCode()); + + TableElement table = TimeTunnelTable.createDefaultTable(); + TimeTunnelTable.drawPlayHeader(className, methodName, objectAddress, index, table); + TimeTunnelTable.drawParameters(advice, table, isNeedExpand(), expand); + + ArthasMethod method = advice.getMethod(); + boolean accessible = advice.getMethod().isAccessible(); + try { + method.setAccessible(true); + Object returnObj = method.invoke(advice.getTarget(), advice.getParams()); + TimeTunnelTable.drawPlayResult(table, returnObj, isNeedExpand(), expand, sizeLimit); + } catch (Throwable t) { + TimeTunnelTable.drawPlayException(table, t, isNeedExpand(), expand); + } finally { + method.setAccessible(accessible); + } + + process.write(RenderUtil.render(table, process.width())) + .write(format("Time fragment[%d] successfully replayed.", index)) + .write("\n"); + affect.rCnt(1); + process.write(affect.toString()).write("\n"); + } finally { + process.end(); + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/TimeTunnelTable.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/TimeTunnelTable.java new file mode 100644 index 00000000..677f6aa9 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/TimeTunnelTable.java @@ -0,0 +1,221 @@ +package com.taobao.arthas.core.command.monitor200; + +import com.taobao.arthas.core.command.express.ExpressException; +import com.taobao.arthas.core.advisor.Advice; +import com.taobao.arthas.core.command.express.ExpressFactory; +import com.taobao.arthas.core.util.StringUtils; +import com.taobao.arthas.core.view.ObjectView; +import com.taobao.text.Decoration; +import com.taobao.text.ui.Element; +import com.taobao.text.ui.LabelElement; +import com.taobao.text.ui.TableElement; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.InvocationTargetException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Map; + +import static com.taobao.text.ui.Element.label; +import static java.lang.Integer.toHexString; + +/** + * @author beiwei30 on 30/11/2016. + */ +public class TimeTunnelTable { + // 各列宽度 + private static final int[] TABLE_COL_WIDTH = new int[]{ + 8, // index + 20, // timestamp + 10, // cost(ms) + 8, // isRet + 8, // isExp + 15, // object address + 30, // class + 30, // method + }; + + // 各列名称 + private static final String[] TABLE_COL_TITLE = new String[]{ + "INDEX", + "TIMESTAMP", + "COST(ms)", + "IS-RET", + "IS-EXP", + "OBJECT", + "CLASS", + "METHOD" + + }; + + static TableElement createTable() { + return new TableElement(TABLE_COL_WIDTH).leftCellPadding(1).rightCellPadding(1); + } + + static TableElement createDefaultTable() { + return new TableElement().leftCellPadding(1).rightCellPadding(1); + } + + static TableElement fillTableHeader(TableElement table) { + LabelElement[] headers = new LabelElement[TABLE_COL_TITLE.length]; + for (int i = 0; i < TABLE_COL_TITLE.length; ++i) { + headers[i] = label(TABLE_COL_TITLE[i]).style(Decoration.bold.bold()); + } + table.row(true, headers); + return table; + } + + // 绘制TimeTunnel表格 + static Element drawTimeTunnelTable(Map timeTunnelMap) { + TableElement table = fillTableHeader(createTable()); + for (Map.Entry entry : timeTunnelMap.entrySet()) { + final int index = entry.getKey(); + final TimeFragment tf = entry.getValue(); + fillTableRow(table, index, tf); + } + return table; + } + + // 填充表格行 + static TableElement fillTableRow(TableElement table, int index, TimeFragment tf) { + Advice advice = tf.getAdvice(); + return table.row( + "" + index, + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(tf.getGmtCreate()), + "" + tf.getCost(), + "" + advice.isAfterReturning(), + "" + advice.isAfterThrowing(), + advice.getTarget() == null + ? "NULL" + : "0x" + toHexString(advice.getTarget().hashCode()), + StringUtils.substringAfterLast("." + advice.getClazz().getName(), "."), + advice.getMethod().getName() + ); + } + + static void drawTimeTunnel(TimeFragment tf, Integer index, TableElement table) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + table.row("INDEX", "" + index) + .row("GMT-CREATE", sdf.format(tf.getGmtCreate())) + .row("COST(ms)", "" + tf.getCost()); + } + + static void drawMethod(Advice advice, String className, String methodName, String objectAddress, TableElement table) { + table.row("OBJECT", objectAddress) + .row("CLASS", className) + .row("METHOD", methodName) + .row("IS-RETURN", "" + advice.isAfterReturning()) + .row("IS-EXCEPTION", "" + advice.isAfterThrowing()); + } + + static void drawThrowException(Advice advice, TableElement table, boolean isNeedExpand, int expandLevel) { + if (advice.isAfterThrowing()) { + //noinspection ThrowableResultOfMethodCallIgnored + Throwable throwable = advice.getThrowExp(); + if (isNeedExpand) { + table.row("THROW-EXCEPTION", new ObjectView(advice.getThrowExp(), expandLevel).draw()); + } else { + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + try { + throwable.printStackTrace(printWriter); + table.row("THROW-EXCEPTION", stringWriter.toString()); + } finally { + printWriter.close(); + } + } + } + } + + static void drawReturnObj(Advice advice, TableElement table, boolean isNeedExpand, int expandLevel, int sizeLimit) { + // fill the returnObj + if (advice.isAfterReturning()) { + if (isNeedExpand) { + table.row("RETURN-OBJ", new ObjectView(advice.getReturnObj(), expandLevel, sizeLimit).draw()); + } else { + table.row("RETURN-OBJ", "" + StringUtils.objectToString(advice.getReturnObj())); + } + } + } + + static void drawParameters(Advice advice, TableElement table, boolean isNeedExpand, int expandLevel) { + // fill the parameters + if (null != advice.getParams()) { + int paramIndex = 0; + for (Object param : advice.getParams()) { + if (isNeedExpand) { + table.row("PARAMETERS[" + paramIndex++ + "]", new ObjectView(param, expandLevel).draw()); + } else { + table.row("PARAMETERS[" + paramIndex++ + "]", "" + StringUtils.objectToString(param)); + } + } + } + } + + static void drawWatchTableHeader(TableElement table) { + table.row(true, label("INDEX").style(Decoration.bold.bold()), label("SEARCH-RESULT") + .style(Decoration.bold.bold())); + } + + static void drawWatchExpress(Map matchingTimeSegmentMap, TableElement table, + String watchExpress, boolean isNeedExpand, int expandLevel, int sizeLimit) + throws ExpressException { + for (Map.Entry entry : matchingTimeSegmentMap.entrySet()) { + Object value = ExpressFactory.newExpress(entry.getValue().getAdvice()).get(watchExpress); + table.row("" + entry.getKey(), "" + + (isNeedExpand ? new ObjectView(value, expandLevel, sizeLimit).draw() : StringUtils.objectToString(value))); + } + } + + static TableElement drawPlayHeader(String className, String methodName, String objectAddress, int index, + TableElement table) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + return table.row("RE-INDEX", "" + index) + .row("GMT-REPLAY", sdf.format(new Date())) + .row("OBJECT", objectAddress) + .row("CLASS", className) + .row("METHOD", methodName); + } + + static void drawPlayResult(TableElement table, Object returnObj, boolean isNeedExpand, int expandLevel, + int sizeLimit) { + // 执行成功:输出成功状态 + table.row("IS-RETURN", "" + true); + table.row("IS-EXCEPTION", "" + false); + + // 执行成功:输出成功结果 + if (isNeedExpand) { + table.row("RETURN-OBJ", new ObjectView(returnObj, expandLevel, sizeLimit).draw()); + } else { + table.row("RETURN-OBJ", "" + StringUtils.objectToString(returnObj)); + } + } + + static void drawPlayException(TableElement table, Throwable t, boolean isNeedExpand, int expandLevel) { + // 执行失败:输出失败状态 + table.row("IS-RETURN", "" + false); + table.row("IS-EXCEPTION", "" + true); + + // 执行失败:输出失败异常信息 + Throwable cause; + if (t instanceof InvocationTargetException) { + cause = t.getCause(); + } else { + cause = t; + } + + if (isNeedExpand) { + table.row("THROW-EXCEPTION", new ObjectView(cause, expandLevel).draw()); + } else { + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + try { + cause.printStackTrace(printWriter); + table.row("THROW-EXCEPTION", stringWriter.toString()); + } finally { + printWriter.close(); + } + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/TraceAdviceListener.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/TraceAdviceListener.java new file mode 100644 index 00000000..f044794c --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/TraceAdviceListener.java @@ -0,0 +1,41 @@ +package com.taobao.arthas.core.command.monitor200; + +import com.taobao.arthas.core.advisor.InvokeTraceable; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.util.StringUtils; + +/** + * @author beiwei30 on 29/11/2016. + */ +public class TraceAdviceListener extends AbstractTraceAdviceListener implements InvokeTraceable { + + /** + * Constructor + */ + public TraceAdviceListener(TraceCommand command, CommandProcess process) { + super(command, process); + } + + /** + * trace 会在被观测的方法体中,在每个方法调用前后插入字节码,所以方法调用开始,结束,抛异常的时候,都会回调下面的接口 + */ + @Override + public void invokeBeforeTracing(String tracingClassName, String tracingMethodName, String tracingMethodDesc) + throws Throwable { + threadBoundEntity.get().view.begin( + StringUtils.normalizeClassName(tracingClassName) + ":" + tracingMethodName + "()"); + } + + @Override + public void invokeAfterTracing(String tracingClassName, String tracingMethodName, String tracingMethodDesc) + throws Throwable { + threadBoundEntity.get().view.end(); + } + + @Override + public void invokeThrowTracing(String tracingClassName, String tracingMethodName, String tracingMethodDesc) + throws Throwable { + threadBoundEntity.get().view.end("throws Exception"); + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/TraceCommand.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/TraceCommand.java new file mode 100644 index 00000000..125284b8 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/TraceCommand.java @@ -0,0 +1,180 @@ +package com.taobao.arthas.core.command.monitor200; + +import com.taobao.arthas.core.advisor.AdviceListener; +import com.taobao.arthas.core.command.Constants; +import com.taobao.arthas.core.shell.cli.Completion; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.util.SearchUtils; +import com.taobao.arthas.core.util.matcher.GroupMatcher; +import com.taobao.arthas.core.util.matcher.Matcher; +import com.taobao.arthas.core.util.matcher.RegexMatcher; +import com.taobao.arthas.core.util.matcher.TrueMatcher; +import com.taobao.arthas.core.util.matcher.WildcardMatcher; +import com.taobao.middleware.cli.annotations.Argument; +import com.taobao.middleware.cli.annotations.Description; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Option; +import com.taobao.middleware.cli.annotations.Summary; + +import java.util.ArrayList; +import java.util.List; + +/** + * 调用跟踪命令
+ * 负责输出一个类中的所有方法调用路径 + * + * @author vlinux on 15/5/27. + */ +@Name("trace") +@Summary("Trace the execution time of specified method invocation.") +@Description(value = Constants.EXPRESS_DESCRIPTION + Constants.EXAMPLE + + " trace -E org\\\\.apache\\\\.commons\\\\.lang\\\\.StringUtils isBlank\n" + + " trace org.apache.commons.lang.StringUtils isBlank\n" + + " trace *StringUtils isBlank\n" + + " trace *StringUtils isBlank params[0].length==1\n" + + " trace *StringUtils isBlank '#cost>100'\n" + + Constants.WIKI + Constants.WIKI_HOME + "cmds/trace") +public class TraceCommand extends EnhancerCommand { + + private String classPattern; + private String methodPattern; + private String conditionExpress; + private boolean isRegEx = false; + private int numberOfLimit = 100; + private List pathPatterns; + private boolean skipJDKTrace; + + @Argument(argName = "class-pattern", index = 0) + @Description("Class name pattern, use either '.' or '/' as separator") + public void setClassPattern(String classPattern) { + this.classPattern = classPattern; + } + + @Argument(argName = "method-pattern", index = 1) + @Description("Method name pattern") + public void setMethodPattern(String methodPattern) { + this.methodPattern = methodPattern; + } + + @Argument(argName = "condition-express", index = 2, required = false) + @Description(Constants.CONDITION_EXPRESS) + public void setConditionExpress(String conditionExpress) { + this.conditionExpress = conditionExpress; + } + + @Option(shortName = "E", longName = "regex", flag = true) + @Description("Enable regular expression to match (wildcard matching by default)") + public void setRegEx(boolean regEx) { + isRegEx = regEx; + } + + @Option(shortName = "n", longName = "limits") + @Description("Threshold of execution times") + public void setNumberOfLimit(int numberOfLimit) { + this.numberOfLimit = numberOfLimit; + } + + @Option(shortName = "p", longName = "path", acceptMultipleValues = true) + @Description("path tracing pattern") + public void setPathPatterns(List pathPatterns) { + this.pathPatterns = pathPatterns; + } + + @Option(shortName = "j", longName = "jdkMethodSkip") + @Description("skip jdk method trace") + public void setSkipJDKTrace(boolean skipJDKTrace) { + this.skipJDKTrace = skipJDKTrace; + } + + public String getClassPattern() { + return classPattern; + } + + public String getMethodPattern() { + return methodPattern; + } + + public String getConditionExpress() { + return conditionExpress; + } + + public boolean isSkipJDKTrace() { + return skipJDKTrace; + } + + public boolean isRegEx() { + return isRegEx; + } + + public int getNumberOfLimit() { + return numberOfLimit; + } + + public List getPathPatterns() { + return pathPatterns; + } + + @Override + protected Matcher getClassNameMatcher() { + if (classNameMatcher == null) { + if (pathPatterns == null || pathPatterns.isEmpty()) { + classNameMatcher = SearchUtils.classNameMatcher(getClassPattern(), isRegEx()); + } else { + classNameMatcher = getPathTracingClassMatcher(); + } + } + return classNameMatcher; + } + + @Override + protected Matcher getMethodNameMatcher() { + if (methodNameMatcher == null) { + if (pathPatterns == null || pathPatterns.isEmpty()) { + methodNameMatcher = SearchUtils.classNameMatcher(getMethodPattern(), isRegEx()); + } else { + methodNameMatcher = getPathTracingMethodMatcher(); + } + } + return methodNameMatcher; + } + + @Override + protected AdviceListener getAdviceListener(CommandProcess process) { + if (pathPatterns == null || pathPatterns.isEmpty()) { + return new TraceAdviceListener(this, process); + } else { + return new PathTraceAdviceListener(this, process); + } + } + + @Override + protected boolean completeExpress(Completion completion) { + completion.complete(EMPTY); + return true; + } + + /** + * 构造追踪路径匹配 + */ + private Matcher getPathTracingClassMatcher() { + + List> matcherList = new ArrayList>(); + matcherList.add(SearchUtils.classNameMatcher(getClassPattern(), isRegEx())); + + if (null != getPathPatterns()) { + for (String pathPattern : getPathPatterns()) { + if (isRegEx()) { + matcherList.add(new RegexMatcher(pathPattern)); + } else { + matcherList.add(new WildcardMatcher(pathPattern)); + } + } + } + + return new GroupMatcher.Or(matcherList); + } + + private Matcher getPathTracingMethodMatcher() { + return new TrueMatcher(); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/TraceEntity.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/TraceEntity.java new file mode 100644 index 00000000..7c5aea17 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/TraceEntity.java @@ -0,0 +1,44 @@ +package com.taobao.arthas.core.command.monitor200; + +import com.taobao.arthas.core.util.DateUtils; +import com.taobao.arthas.core.util.ThreadUtil; +import com.taobao.arthas.core.view.TreeView; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * 用于在ThreadLocal中传递的实体 + * @author ralf0131 2017-01-05 14:05. + */ +public class TraceEntity { + + protected TreeView view; + protected int deep; + + public TraceEntity() { + this.view = createTreeView(); + this.deep = 0; + } + + public TreeView getView() { + return view; + } + + public void setView(TreeView view) { + this.view = view; + } + + public int getDeep() { + return deep; + } + + public void setDeep(int deep) { + this.deep = deep; + } + + private TreeView createTreeView() { + String threadTitle = "ts=" + DateUtils.getCurrentDate()+ ";" + ThreadUtil.getThreadTitle(Thread.currentThread()); + return new TreeView(true, threadTitle); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/WatchAdviceListener.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/WatchAdviceListener.java new file mode 100644 index 00000000..dcd655a0 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/WatchAdviceListener.java @@ -0,0 +1,102 @@ +package com.taobao.arthas.core.command.monitor200; + +import com.taobao.arthas.core.advisor.ReflectAdviceListenerAdapter; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.advisor.Advice; +import com.taobao.arthas.core.advisor.ArthasMethod; +import com.taobao.arthas.core.util.DateUtils; +import com.taobao.arthas.core.util.LogUtil; +import com.taobao.arthas.core.util.StringUtils; +import com.taobao.arthas.core.util.ThreadLocalWatch; +import com.taobao.arthas.core.view.ObjectView; +import com.taobao.middleware.logger.Logger; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * @author beiwei30 on 29/11/2016. + */ +class WatchAdviceListener extends ReflectAdviceListenerAdapter { + + private static final Logger logger = LogUtil.getArthasLogger(); + private final ThreadLocalWatch threadLocalWatch = new ThreadLocalWatch(); + private WatchCommand command; + private CommandProcess process; + + public WatchAdviceListener(WatchCommand command, CommandProcess process) { + this.command = command; + this.process = process; + } + + private boolean isFinish() { + return command.isFinish() || !command.isBefore() && !command.isException() && !command.isSuccess(); + } + + @Override + public void before(ClassLoader loader, Class clazz, ArthasMethod method, Object target, Object[] args) + throws Throwable { + // 开始计算本次方法调用耗时 + threadLocalWatch.start(); + if (command.isBefore()) { + watching(Advice.newForBefore(loader, clazz, method, target, args)); + } + } + + @Override + public void afterReturning(ClassLoader loader, Class clazz, ArthasMethod method, Object target, Object[] args, + Object returnObject) throws Throwable { + Advice advice = Advice.newForAfterRetuning(loader, clazz, method, target, args, returnObject); + if (command.isSuccess()) { + watching(advice); + } + + finishing(advice); + } + + @Override + public void afterThrowing(ClassLoader loader, Class clazz, ArthasMethod method, Object target, Object[] args, + Throwable throwable) { + Advice advice = Advice.newForAfterThrowing(loader, clazz, method, target, args, throwable); + if (command.isException()) { + watching(advice); + } + + finishing(advice); + } + + private void finishing(Advice advice) { + if (isFinish()) { + watching(advice); + } + } + + private boolean isNeedExpand() { + Integer expand = command.getExpand(); + return null != expand && expand >= 0; + } + + private void watching(Advice advice) { + try { + // 本次调用的耗时 + double cost = threadLocalWatch.costInMillis(); + if (isConditionMet(command.getConditionExpress(), advice, cost)) { + // TODO: concurrency issues for process.write + Object value = getExpressionResult(command.getExpress(), advice, cost); + String result = StringUtils.objectToString( + isNeedExpand() ? new ObjectView(value, command.getExpand(), command.getSizeLimit()).draw() : value); + process.write("ts=" + DateUtils.getCurrentDate() + ";result=" + result + "\n"); + process.times().incrementAndGet(); + if (isLimitExceeded(command.getNumberOfLimit(), process.times().get())) { + abortProcess(process, command.getNumberOfLimit()); + } + } + } catch (Exception e) { + logger.warn("watch failed.", e); + process.write("watch failed, condition is: " + command.getConditionExpress() + ", express is: " + + command.getExpress() + ", " + e.getMessage() + ", visit " + LogUtil.LOGGER_FILE + + " for more details.\n"); + process.end(); + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/WatchCommand.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/WatchCommand.java new file mode 100644 index 00000000..36268d45 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/WatchCommand.java @@ -0,0 +1,180 @@ +package com.taobao.arthas.core.command.monitor200; + +import com.taobao.arthas.core.advisor.AdviceListener; +import com.taobao.arthas.core.command.Constants; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.util.SearchUtils; +import com.taobao.arthas.core.util.matcher.Matcher; +import com.taobao.middleware.cli.annotations.Argument; +import com.taobao.middleware.cli.annotations.Description; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Option; +import com.taobao.middleware.cli.annotations.Summary; + +@Name("watch") +@Summary("Display the input/output parameter, return object, and thrown exception of specified method invocation") +@Description(Constants.EXPRESS_DESCRIPTION + "\nExamples:\n" + + " watch -Eb org\\.apache\\.commons\\.lang\\.StringUtils isBlank params[0]\n" + + " watch -b org.apache.commons.lang.StringUtils isBlank params[0]\n" + + " watch -f org.apache.commons.lang.StringUtils isBlank returnObj\n" + + " watch -bf *StringUtils isBlank params[0]\n" + + " watch *StringUtils isBlank params[0]\n" + + " watch *StringUtils isBlank params[0] params[0].length==1\n" + + " watch *StringUtils isBlank '#cost>100'\n" + + Constants.WIKI + Constants.WIKI_HOME + "cmds/watch") +public class WatchCommand extends EnhancerCommand { + + private String classPattern; + private String methodPattern; + private String express; + private String conditionExpress; + private boolean isBefore = false; + private boolean isFinish = false; + private boolean isException = false; + private boolean isSuccess = false; + private Integer expand = 1; + private Integer sizeLimit = 10 * 1024 * 1024; + private boolean isRegEx = false; + private int numberOfLimit = 100; + + @Argument(index = 0, argName = "class-pattern") + @Description("The full qualified class name you want to watch") + public void setClassPattern(String classPattern) { + this.classPattern = classPattern; + } + + @Argument(index = 1, argName = "method-pattern") + @Description("The method name you want to watch") + public void setMethodPattern(String methodPattern) { + this.methodPattern = methodPattern; + } + + @Argument(index = 2, argName = "express") + @Description("the content you want to watch, written by ognl.\n" + Constants.EXPRESS_EXAMPLES) + public void setExpress(String express) { + this.express = express; + } + + @Argument(index = 3, argName = "condition-express", required = false) + @Description(Constants.CONDITION_EXPRESS) + public void setConditionExpress(String conditionExpress) { + this.conditionExpress = conditionExpress; + } + + @Option(shortName = "b", longName = "before", flag = true) + @Description("Watch before invocation") + public void setBefore(boolean before) { + isBefore = before; + } + + @Option(shortName = "f", longName = "finish", flag = true) + @Description("Watch after invocation, enable by default") + public void setFinish(boolean finish) { + isFinish = finish; + } + + @Option(shortName = "e", longName = "exception", flag = true) + @Description("Watch after throw exception") + public void setException(boolean exception) { + isException = exception; + } + + @Option(shortName = "s", longName = "success", flag = true) + @Description("Watch after successful invocation") + public void setSuccess(boolean success) { + isSuccess = success; + } + + @Option(shortName = "M", longName = "sizeLimit") + @Description("Upper size limit in bytes for the result (10 * 1024 * 1024 by default)") + public void setSizeLimit(Integer sizeLimit) { + this.sizeLimit = sizeLimit; + } + + @Option(shortName = "x", longName = "expand") + @Description("Expand level of object (1 by default)") + public void setExpand(Integer expand) { + this.expand = expand; + } + + @Option(shortName = "E", longName = "regex", flag = true) + @Description("Enable regular expression to match (wildcard matching by default)") + public void setRegEx(boolean regEx) { + isRegEx = regEx; + } + + @Option(shortName = "n", longName = "limits") + @Description("Threshold of execution times") + public void setNumberOfLimit(int numberOfLimit) { + this.numberOfLimit = numberOfLimit; + } + + public String getClassPattern() { + return classPattern; + } + + public String getMethodPattern() { + return methodPattern; + } + + public String getExpress() { + return express; + } + + public String getConditionExpress() { + return conditionExpress; + } + + public boolean isBefore() { + return isBefore; + } + + public boolean isFinish() { + return isFinish; + } + + public boolean isException() { + return isException; + } + + public boolean isSuccess() { + return isSuccess; + } + + public Integer getExpand() { + return expand; + } + + public Integer getSizeLimit() { + return sizeLimit; + } + + public boolean isRegEx() { + return isRegEx; + } + + public int getNumberOfLimit() { + return numberOfLimit; + } + + @Override + protected Matcher getClassNameMatcher() { + if (classNameMatcher == null) { + classNameMatcher = SearchUtils.classNameMatcher(getClassPattern(), isRegEx()); + } + return classNameMatcher; + } + + @Override + protected Matcher getMethodNameMatcher() { + if (methodNameMatcher == null) { + methodNameMatcher = SearchUtils.classNameMatcher(getMethodPattern(), isRegEx()); + } + return methodNameMatcher; + } + + @Override + protected AdviceListener getAdviceListener(CommandProcess process) { + return new WatchAdviceListener(this, process); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/config/Configure.java b/core/src/main/java/com/taobao/arthas/core/config/Configure.java new file mode 100644 index 00000000..2b655abe --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/config/Configure.java @@ -0,0 +1,135 @@ +package com.taobao.arthas.core.config; + +import com.taobao.arthas.core.util.reflect.ArthasReflectUtils; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +import static java.lang.reflect.Modifier.isStatic; + +/** + * 配置类 + * + * @author vlinux + */ +public class Configure { + + private String ip; + private int telnetPort; + private int httpPort; + private int javaPid; + private String arthasCore; + private String arthasAgent; + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = normalizeIp(ip); + } + + public int getTelnetPort() { + return telnetPort; + } + + public void setTelnetPort(int telnetPort) { + this.telnetPort = telnetPort; + } + + public void setHttpPort(int httpPort) { + this.httpPort = httpPort; + } + + public int getHttpPort() { + return httpPort; + } + + public int getJavaPid() { + return javaPid; + } + + public void setJavaPid(int javaPid) { + this.javaPid = javaPid; + } + + public String getArthasAgent() { + return arthasAgent; + } + + public void setArthasAgent(String arthasAgent) { + this.arthasAgent = arthasAgent; + } + + public String getArthasCore() { + return arthasCore; + } + + public void setArthasCore(String arthasCore) { + this.arthasCore = arthasCore; + } + + // 对象的编码解码器 + private final static FeatureCodec codec = new FeatureCodec(';', '='); + + /** + * 序列化成字符串 + * + * @return 序列化字符串 + */ + @Override + public String toString() { + + final Map map = new HashMap(); + for (Field field : ArthasReflectUtils.getFields(Configure.class)) { + + // 过滤掉静态类 + if (isStatic(field.getModifiers())) { + continue; + } + + // 非静态的才需要纳入非序列化过程 + try { + map.put(field.getName(), String.valueOf(ArthasReflectUtils.getFieldValueByField(this, field))); + } catch (Throwable t) { + // + } + + } + + return codec.toString(map); + } + + /** + * 反序列化字符串成对象 + * + * @param toString 序列化字符串 + * @return 反序列化的对象 + */ + public static Configure toConfigure(String toString) { + final Configure configure = new Configure(); + final Map map = codec.toMap(toString); + + for (Map.Entry entry : map.entrySet()) { + try { + final Field field = ArthasReflectUtils.getField(Configure.class, entry.getKey()); + if (null != field && !isStatic(field.getModifiers())) { + ArthasReflectUtils.set(field, ArthasReflectUtils.valueOf(field.getType(), entry.getValue()), configure); + } + } catch (Throwable t) { + // + } + } + return configure; + } + + private String normalizeIp(String ip){ + if ("127.0.0.1".equals(ip)) { + // bind to all network interfaces, allowing remote connections + return "0.0.0.0"; + } + return ip; + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/config/FeatureCodec.java b/core/src/main/java/com/taobao/arthas/core/config/FeatureCodec.java new file mode 100644 index 00000000..250dff9f --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/config/FeatureCodec.java @@ -0,0 +1,243 @@ +package com.taobao.arthas.core.config; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Stack; + +import static com.taobao.arthas.core.util.ArthasCheckUtils.isEquals; +import static com.taobao.arthas.core.util.ArthasCheckUtils.isIn; +import static com.taobao.arthas.core.util.StringUtils.isBlank; + +/** + * Feature编解器(线程安全)
+ *

+ * 用于封装系统内部features/attribute等扩展字段的管理 + * Created by dukun on 15/3/31. + */ +public class FeatureCodec { + + /** + * KV片段分割符
+ * KV片段定义为一个完整的KV对,例如字符串;k1=v1;k2=v2; + * 其中;即为KV片段分隔符 + */ + private final char kvSegmentSeparator; + + /** + * KV分割符
+ * KV定义为一个KV对区分K和V的分割符号,例如字符串k1=v1 + * 其中=即为KV分隔符 + */ + private final char kvSeparator; + + /** + * 转义前缀符 + */ + private static final char ESCAPE_PREFIX_CHAR = '\\'; + + /** + * 使用指定的KV分割符构造FeatureParser
+ * + * @param kvSegmentSeparator KV对之间的分隔符 + * @param kvSeparator K与V之间的分隔符 + */ + public FeatureCodec(final char kvSegmentSeparator, final char kvSeparator) { + + // 分隔符禁止与转义前缀符相等 + if (isIn(ESCAPE_PREFIX_CHAR, kvSegmentSeparator, kvSeparator)) { + throw new IllegalArgumentException("separator can not init to '" + ESCAPE_PREFIX_CHAR + "'."); + } + + this.kvSegmentSeparator = kvSegmentSeparator; + this.kvSeparator = kvSeparator; + } + + /** + * map集合转换到feature字符串 + * + * @param map map集合 + * @return feature字符串 + */ + public String toString(final Map map) { + + final StringBuilder featureSB = new StringBuilder().append(kvSegmentSeparator); + + if (null == map + || map.isEmpty()) { + return featureSB.toString(); + } + + for (Map.Entry entry : map.entrySet()) { + + featureSB + .append(escapeEncode(entry.getKey())) + .append(kvSeparator) + .append(escapeEncode(entry.getValue())) + .append(kvSegmentSeparator) + ; + + } + + return featureSB.toString(); + } + + + /** + * feature字符串转换到map集合 + * + * @param featureString the feature string + * @return the map + */ + public Map toMap(final String featureString) { + + final Map map = new HashMap(); + + if (isBlank(featureString)) { + return map; + } + + for (String kv : escapeSplit(featureString, kvSegmentSeparator)) { + + if (isBlank(kv)) { + // 过滤掉为空的字符串片段 + continue; + } + + final String[] ar = escapeSplit(kv, kvSeparator); + if (ar.length != 2) { + // 过滤掉不符合K:V单目的情况 + continue; + } + + final String k = ar[0]; + final String v = ar[1]; + if (!isBlank(k) + && !isBlank(v)) { + map.put(escapeDecode(k), escapeDecode(v)); + } + + } + + return map; + } + + /** + * 转义编码 + * + * @param string 原始字符串 + * @return 转义编码后的字符串 + */ + private String escapeEncode(final String string) { + final StringBuilder returnSB = new StringBuilder(); + for (final char c : string.toCharArray()) { + if (isIn(c, kvSegmentSeparator, kvSeparator, ESCAPE_PREFIX_CHAR)) { + returnSB.append(ESCAPE_PREFIX_CHAR); + } + returnSB.append(c); + } + + return returnSB.toString(); + } + + /** + * 转义解码 + * + * @param string 编码字符串 + * @return 转义解码后的字符串 + */ + private String escapeDecode(String string) { + + final StringBuilder segmentSB = new StringBuilder(); + final int stringLength = string.length(); + + for (int index = 0; index < stringLength; index++) { + + final char c = string.charAt(index); + + if (isEquals(c, ESCAPE_PREFIX_CHAR) + && index < stringLength - 1) { + + final char nextChar = string.charAt(++index); + + // 下一个字符是转义符 + if (isIn(nextChar, kvSegmentSeparator, kvSeparator, ESCAPE_PREFIX_CHAR)) { + segmentSB.append(nextChar); + } + + // 如果不是转义字符,则需要两个都放入 + else { + segmentSB.append(c); + segmentSB.append(nextChar); + } + } else { + segmentSB.append(c); + } + + } + + return segmentSB.toString(); + } + + /** + * 编码字符串拆分 + * + * @param string 编码字符串 + * @param splitEscapeChar 分割符 + * @return 拆分后的字符串数组 + */ + private String[] escapeSplit(String string, char splitEscapeChar) { + + final ArrayList segmentArrayList = new ArrayList(); + final Stack decodeStack = new Stack(); + final int stringLength = string.length(); + + for (int index = 0; index < stringLength; index++) { + + boolean isArchive = false; + + final char c = string.charAt(index); + + // 匹配到转义前缀符 + if (isEquals(c, ESCAPE_PREFIX_CHAR)) { + + decodeStack.push(c); + if (index < stringLength - 1) { + final char nextChar = string.charAt(++index); + decodeStack.push(nextChar); + } + + } + + // 匹配到分割符 + else if (isEquals(c, splitEscapeChar)) { + isArchive = true; + } + + // 匹配到其他字符 + else { + decodeStack.push(c); + } + + if (isArchive + || index == stringLength - 1) { + final StringBuilder segmentSB = new StringBuilder(decodeStack.size()); + while (!decodeStack.isEmpty()) { + segmentSB.append(decodeStack.pop()); + } + + segmentArrayList.add( + segmentSB + .reverse() // 因为堆栈中是逆序的,所以需要对逆序的字符串再次逆序 + .toString() // toString + .trim() // 考虑到字符串片段可能会出现首尾空格的场景,这里做一个过滤 + ); + } + + } + + return segmentArrayList.toArray(new String[segmentArrayList.size()]); + } + + +} \ No newline at end of file diff --git a/core/src/main/java/com/taobao/arthas/core/server/ArthasBootstrap.java b/core/src/main/java/com/taobao/arthas/core/server/ArthasBootstrap.java new file mode 100644 index 00000000..2661bdac --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/server/ArthasBootstrap.java @@ -0,0 +1,180 @@ +package com.taobao.arthas.core.server; + +import com.taobao.arthas.core.config.Configure; +import com.taobao.arthas.core.command.BuiltinCommandPack; +import com.taobao.arthas.core.shell.ShellServer; +import com.taobao.arthas.core.shell.ShellServerOptions; +import com.taobao.arthas.core.shell.command.CommandResolver; +import com.taobao.arthas.core.shell.handlers.BindHandler; +import com.taobao.arthas.core.shell.impl.ShellServerImpl; +import com.taobao.arthas.core.shell.term.impl.HttpTermServer; +import com.taobao.arthas.core.shell.term.impl.TelnetTermServer; +import com.taobao.arthas.core.util.Constants; +import com.taobao.arthas.core.util.LogUtil; +import com.taobao.arthas.core.util.UserStatUtil; +import com.taobao.middleware.logger.Logger; + +import java.io.IOException; +import java.lang.instrument.Instrumentation; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicBoolean; + + +/** + * @author vlinux on 15/5/2. + */ +public class ArthasBootstrap { + + private static Logger logger = LogUtil.getArthasLogger(); + private static ArthasBootstrap arthasBootstrap; + + private AtomicBoolean isBindRef = new AtomicBoolean(false); + private int pid; + private Instrumentation instrumentation; + private Thread shutdown; + private ShellServer shellServer; + private ExecutorService executorService; + + private ArthasBootstrap(int pid, Instrumentation instrumentation) { + this.pid = pid; + this.instrumentation = instrumentation; + + executorService = Executors.newCachedThreadPool(new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + final Thread t = new Thread(r, "as-command-execute-daemon"); + t.setDaemon(true); + return t; + } + }); + + shutdown = new Thread("as-shutdown-hooker") { + + @Override + public void run() { + ArthasBootstrap.this.destroy(); + } + }; + + Runtime.getRuntime().addShutdownHook(shutdown); + } + + /** + * Bootstrap arthas server + * + * @param configure 配置信息 + * @throws IOException 服务器启动失败 + */ + public void bind(Configure configure) throws Throwable { + + long start = System.currentTimeMillis(); + + if (!isBindRef.compareAndSet(false, true)) { + throw new IllegalStateException("already bind"); + } + + try { + ShellServerOptions options = new ShellServerOptions().setInstrumentation(instrumentation).setPid(pid); + shellServer = new ShellServerImpl(options, this); + BuiltinCommandPack builtinCommands = new BuiltinCommandPack(); + List resolvers = new ArrayList(); + resolvers.add(builtinCommands); + // TODO: discover user provided command resolver + shellServer.registerTermServer(new TelnetTermServer( + configure.getIp(), configure.getTelnetPort(), options.getConnectionTimeout())); + shellServer.registerTermServer(new HttpTermServer( + configure.getIp(), configure.getHttpPort(), options.getConnectionTimeout())); + + for (CommandResolver resolver : resolvers) { + shellServer.registerCommandResolver(resolver); + } + + shellServer.listen(new BindHandler(isBindRef)); + + logger.info("as-server listening on network={};telnet={};http={};timeout={};", configure.getIp(), + configure.getTelnetPort(), configure.getHttpPort(), options.getConnectionTimeout()); + // 异步回报启动次数 + UserStatUtil.arthasStart(); + + logger.info("as-server started in {} ms", System.currentTimeMillis() - start ); + } catch (Throwable e) { + logger.error(null, "Error during bind to port " + configure.getTelnetPort(), e); + if (shellServer != null) { + shellServer.close(); + } + throw e; + } + } + + /** + * 判断服务端是否已经启动 + * + * @return true:服务端已经启动;false:服务端关闭 + */ + public boolean isBind() { + return isBindRef.get(); + } + + public void destroy() { + executorService.shutdownNow(); + UserStatUtil.destroy(); + // clear the reference in Spy class. + cleanUpSpyReference(); + try { + Runtime.getRuntime().removeShutdownHook(shutdown); + } catch (Throwable t) { + // ignore + } + logger.info("as-server destroy completed."); + // see middleware-container/arthas/issues/123 + LogUtil.closeResultLogger(); + } + + /** + * 单例 + * + * @param instrumentation JVM增强 + * @return ArthasServer单例 + */ + public synchronized static ArthasBootstrap getInstance(int javaPid, Instrumentation instrumentation) { + if (arthasBootstrap == null) { + arthasBootstrap = new ArthasBootstrap(javaPid, instrumentation); + } + return arthasBootstrap; + } + + /** + * @return ArthasServer单例 + */ + public static ArthasBootstrap getInstance() { + if (arthasBootstrap == null) { + throw new IllegalStateException("ArthasBootstrap must be initialized before!"); + } + return arthasBootstrap; + } + + public void execute(Runnable command) { + executorService.execute(command); + } + + /** + * 清除spy中对classloader的引用,避免内存泄露 + */ + private void cleanUpSpyReference() { + try { + // 从ArthasClassLoader中加载Spy + Class spyClass = this.getClass().getClassLoader().loadClass(Constants.SPY_CLASSNAME); + Method agentDestroyMethod = spyClass.getMethod("destroy"); + agentDestroyMethod.invoke(null); + } catch (ClassNotFoundException e) { + logger.error(null, "Spy load failed from ArthasClassLoader, which should not happen", e); + } catch (Exception e) { + logger.error(null, "Spy destroy failed: ", e); + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/Shell.java b/core/src/main/java/com/taobao/arthas/core/shell/Shell.java new file mode 100644 index 00000000..5801bf94 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/Shell.java @@ -0,0 +1,45 @@ +package com.taobao.arthas.core.shell; + +import com.taobao.arthas.core.shell.cli.CliToken; +import com.taobao.arthas.core.shell.session.Session; +import com.taobao.arthas.core.shell.system.Job; +import com.taobao.arthas.core.shell.system.JobController; + +import java.util.List; + +/** + * An interactive session between a consumer and a shell. + * + * @author Julien Viet + */ +public interface Shell { + + /** + * Create a job, the created job should then be executed with the {@link Job#run()} method. + * + * @param line the command line creating this job + * @return the created job + */ + Job createJob(List line); + + /** + * See {@link #createJob(List)} + */ + Job createJob(String line); + + /** + * @return the shell's job controller + */ + JobController jobController(); + + /** + * @return the current shell session + */ + Session session(); + + /** + * Close the shell. + */ + void close(String reason); +} + diff --git a/core/src/main/java/com/taobao/arthas/core/shell/ShellServer.java b/core/src/main/java/com/taobao/arthas/core/shell/ShellServer.java new file mode 100644 index 00000000..49192c84 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/ShellServer.java @@ -0,0 +1,105 @@ +package com.taobao.arthas.core.shell; + +import com.taobao.arthas.core.shell.command.CommandResolver; +import com.taobao.arthas.core.shell.future.Future; +import com.taobao.arthas.core.shell.handlers.Handler; +import com.taobao.arthas.core.shell.handlers.NoOpHandler; +import com.taobao.arthas.core.shell.impl.ShellServerImpl; +import com.taobao.arthas.core.shell.term.Term; +import com.taobao.arthas.core.shell.term.TermServer; + +/** + * The shell server.

+ *

+ * A shell server is associated with a collection of {@link TermServer term servers}: the {@link #registerTermServer(TermServer)} + * method registers a term server. Term servers life cycle are managed by this server.

+ *

+ * When a {@link TermServer term server} receives an incoming connection, a {@link com.taobao.arthas.core.shell.system.JobController} instance is created and + * associated with this connection.

+ *

+ * The {@link #createShell()} method can be used to create {@link com.taobao.arthas.core.shell.system.JobController} instance for testing purposes. + * + * @author Julien Viet + */ +public abstract class ShellServer { + + /** + * Create a new shell server with default options. + * + * @param options the options + * @return the created shell server + */ + public static ShellServer create(ShellServerOptions options) { + return new ShellServerImpl(options); + } + + /** + * Create a new shell server with specific options. + * + * @return the created shell server + */ + public static ShellServer create() { + return new ShellServerImpl(new ShellServerOptions()); + } + + /** + * Register a command resolver for this server. + * + * @param resolver the resolver + * @return a reference to this, so the API can be used fluently + */ + public abstract ShellServer registerCommandResolver(CommandResolver resolver); + + /** + * Register a term server to this shell server, the term server lifecycle methods are managed by this shell server. + * + * @param termServer the term server to add + * @return a reference to this, so the API can be used fluently + */ + public abstract ShellServer registerTermServer(TermServer termServer); + + /** + * Create a new shell, the returned shell should be closed explicitely. + * + * @param term the shell associated terminal + * @return the created shell + */ + public abstract Shell createShell(Term term); + + /** + * Create a new shell, the returned shell should be closed explicitely. + * + * @return the created shell + */ + public abstract Shell createShell(); + + /** + * Start the shell service, this is an asynchronous start. + */ + @SuppressWarnings("unchecked") + public ShellServer listen() { + return listen(new NoOpHandler()); + } + + /** + * Start the shell service, this is an asynchronous start. + * + * @param listenHandler handler for getting notified when service is started + */ + public abstract ShellServer listen(Handler> listenHandler); + + /** + * Close the shell server, this is an asynchronous close. + */ + @SuppressWarnings("unchecked") + public void close() { + close(new NoOpHandler()); + } + + /** + * Close the shell server, this is an asynchronous close. + * + * @param completionHandler handler for getting notified when service is stopped + */ + public abstract void close(Handler> completionHandler); +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/ShellServerOptions.java b/core/src/main/java/com/taobao/arthas/core/shell/ShellServerOptions.java new file mode 100644 index 00000000..daf5624d --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/ShellServerOptions.java @@ -0,0 +1,126 @@ +package com.taobao.arthas.core.shell; + +import com.taobao.arthas.core.util.ArthasBanner; + +import java.lang.instrument.Instrumentation; + +/** + * The configurations options for the shell server. + * + * @author Julien Viet + */ +public class ShellServerOptions { + + /** + * Default of how often, in ms, to check for expired sessions + */ + public static final long DEFAULT_REAPER_INTERVAL = 60 * 1000; // 60 seconds + + /** + * Default time, in ms, that a shell session lasts for without being accessed before expiring. + */ + public static final long DEFAULT_SESSION_TIMEOUT = 5 * 60 * 1000; // 5 minutes + + /** + * Default time, in ms, that a server waits for a client to connect + */ + public static final long DEFAULT_CONNECTION_TIMEOUT = 6000; // 6 seconds + + public static final String DEFAULT_WELCOME_MESSAGE = ArthasBanner.welcome(); + + public static final String DEFAULT_INPUTRC = "com/taobao/arthas/core/shell/term/readline/inputrc"; + + private String welcomeMessage; + private long sessionTimeout; + private long reaperInterval; + private long connectionTimeout; + private int pid; + private Instrumentation instrumentation; + + public ShellServerOptions() { + welcomeMessage = DEFAULT_WELCOME_MESSAGE; + sessionTimeout = DEFAULT_SESSION_TIMEOUT; + connectionTimeout = DEFAULT_CONNECTION_TIMEOUT; + reaperInterval = DEFAULT_REAPER_INTERVAL; + } + + /** + * @return the shell welcome message + */ + public String getWelcomeMessage() { + return welcomeMessage; + } + + /** + * Set the shell welcome message, i.e the message displayed in the user console when he connects to the shell. + * + * @param welcomeMessage the welcome message + * @return a reference to this, so the API can be used fluently + */ + public ShellServerOptions setWelcomeMessage(String welcomeMessage) { + this.welcomeMessage = welcomeMessage; + return this; + } + + /** + * @return the session timeout + */ + public long getSessionTimeout() { + return sessionTimeout; + } + + /** + * Set the session timeout. + * + * @param sessionTimeout the new session timeout + * @return a reference to this, so the API can be used fluently + */ + public ShellServerOptions setSessionTimeout(long sessionTimeout) { + this.sessionTimeout = sessionTimeout; + return this; + } + + /** + * @return the reaper interval + */ + public long getReaperInterval() { + return reaperInterval; + } + + /** + * Set the repear interval, i.e the period at which session eviction is performed. + * + * @param reaperInterval the new repeat interval + * @return a reference to this, so the API can be used fluently + */ + public ShellServerOptions setReaperInterval(long reaperInterval) { + this.reaperInterval = reaperInterval; + return this; + } + + public ShellServerOptions setPid(int pid) { + this.pid = pid; + return this; + } + + public ShellServerOptions setInstrumentation(Instrumentation instrumentation) { + this.instrumentation = instrumentation; + return this; + } + + public int getPid() { + return pid; + } + + public Instrumentation getInstrumentation() { + return instrumentation; + } + + public long getConnectionTimeout() { + return connectionTimeout; + } + + public void setConnectionTimeout(long connectionTimeout) { + this.connectionTimeout = connectionTimeout; + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/cli/CliToken.java b/core/src/main/java/com/taobao/arthas/core/shell/cli/CliToken.java new file mode 100644 index 00000000..c342693b --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/cli/CliToken.java @@ -0,0 +1,23 @@ +package com.taobao.arthas.core.shell.cli; + +public interface CliToken { + /** + * @return the token value + */ + String value(); + + /** + * @return the raw token value, that may contain unescaped chars, for instance {@literal "ab\"cd"} + */ + String raw(); + + /** + * @return true when it's a text token + */ + boolean isText(); + + /** + * @return true when it's a blank token + */ + boolean isBlank(); +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/cli/CliTokens.java b/core/src/main/java/com/taobao/arthas/core/shell/cli/CliTokens.java new file mode 100644 index 00000000..c34f20c2 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/cli/CliTokens.java @@ -0,0 +1,40 @@ +package com.taobao.arthas.core.shell.cli; + +import com.taobao.arthas.core.shell.cli.impl.CliTokenImpl; + +import java.util.List; + +/** + * @author beiwei30 on 09/11/2016. + */ +public class CliTokens { + /** + * Create a text token. + * + * @param text the text + * @return the token + */ + public static CliToken createText(String text) { + return new CliTokenImpl(true, text, text); + } + + /** + * Create a new blank token. + * + * @param blank the blank value + * @return the token + */ + public static CliToken createBlank(String blank) { + return new CliTokenImpl(false, blank, blank); + } + + /** + * Tokenize the string argument and return a list of tokens. + * + * @param s the tokenized string + * @return the tokens + */ + public static List tokenize(String s) { + return CliTokenImpl.tokenize(s); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/cli/Completion.java b/core/src/main/java/com/taobao/arthas/core/shell/cli/Completion.java new file mode 100644 index 00000000..bbb8f899 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/cli/Completion.java @@ -0,0 +1,44 @@ +package com.taobao.arthas.core.shell.cli; + +import com.taobao.arthas.core.shell.session.Session; + +import java.util.List; + +/** + * The completion object + * + * @author Julien Viet + */ +public interface Completion { + + /** + * @return the shell current session, useful for accessing data like the current path for file completion, etc... + */ + Session session(); + + /** + * @return the current line being completed in raw format, i.e without any char escape performed + */ + String rawLine(); + + /** + * @return the current line being completed as preparsed tokens + */ + List lineTokens(); + + /** + * End the completion with a list of candidates, these candidates will be displayed by the shell on the console. + * + * @param candidates the candidates + */ + void complete(List candidates); + + /** + * End the completion with a value that will be inserted to complete the line. + * + * @param value the value to complete with + * @param terminal true if the value is terminal, i.e can be further completed + */ + void complete(String value, boolean terminal); + +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/cli/CompletionUtils.java b/core/src/main/java/com/taobao/arthas/core/shell/cli/CompletionUtils.java new file mode 100644 index 00000000..e0477b09 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/cli/CompletionUtils.java @@ -0,0 +1,108 @@ +package com.taobao.arthas.core.shell.cli; + +import com.taobao.arthas.core.shell.session.Session; +import com.taobao.arthas.core.shell.term.Tty; +import com.taobao.arthas.core.util.usage.StyledUsageFormatter; +import com.taobao.middleware.cli.CLI; +import com.taobao.middleware.cli.Option; +import com.taobao.middleware.cli.annotations.CLIConfigurator; +import io.termd.core.util.Helper; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +/** + * @author beiwei30 on 09/11/2016. + */ +public class CompletionUtils { + + public static String findLongestCommonPrefix(Collection values) { + List entries = new LinkedList(); + for (String value : values) { + int[] entry = Helper.toCodePoints(value); + entries.add(entry); + } + return Helper.fromCodePoints(io.termd.core.readline.Completion.findLongestCommonPrefix(entries)); + } + + public static void complete(Completion completion, Class clazz) { + List tokens = completion.lineTokens(); + CliToken lastToken = tokens.get(tokens.size() - 1); + CLI cli = CLIConfigurator.define(clazz); + List options = cli.getOptions(); + if (lastToken == null || lastToken.isBlank()) { + completeUsage(completion, cli); + } else if (lastToken.value().startsWith("--")) { + completeLongOption(completion, lastToken, options); + } else if (lastToken.value().startsWith("-")) { + completeShortOption(completion, lastToken, options); + } else { + completion.complete(Collections.emptyList()); + } + } + + /** + * 从给定的查询数组中查询匹配的对象,并进行自动补全 + */ + public static boolean complete(Completion completion, Collection searchScope) { + List tokens = completion.lineTokens(); + CliToken lastToken = tokens.get(tokens.size() - 1); + List candidates = new ArrayList(); + for (String name: searchScope) { + if (" ".equals(lastToken.value()) || name.startsWith(lastToken.value())) { + candidates.add(name); + } + } + if (candidates.size() == 1) { + completion.complete(candidates.get(0).substring(lastToken.value().length()), true); + return true; + } else { + completion.complete(candidates); + return false; + } + } + + public static void completeShortOption(Completion completion, CliToken lastToken, List

+ *

+ * It is a mutable command resolver. + * + * @author Julien Viet + */ +public class CommandRegistry implements CommandResolver { + final ConcurrentHashMap commandMap = new ConcurrentHashMap(); + + /** + * Create a new registry. + * + * @return the created registry + */ + public static CommandRegistry create() { + return new CommandRegistry(); + } + + /** + * Register a single command. + */ + public CommandRegistry registerCommand(Class command) { + return registerCommand(Command.create(command)); + } + + /** + * Register a command + * + * @param command the command to register + * @return a reference to this, so the API can be used fluently + */ + public CommandRegistry registerCommand(Command command) { + return registerCommands(Collections.singletonList(command)); + } + + /** + * Register a list of commands. + * + * @param commands the commands to register + * @return a reference to this, so the API can be used fluently + */ + public CommandRegistry registerCommands(List commands) { + for (Command command : commands) { + commandMap.put(command.name(), command); + } + return this; + } + + + /** + * Unregister a command. + * + * @param commandName the command name + * @return a reference to this, so the API can be used fluently + */ + public CommandRegistry unregisterCommand(String commandName) { + commandMap.remove(commandName); + return this; + } + + @Override + public List commands() { + return new ArrayList(commandMap.values()); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/command/CommandResolver.java b/core/src/main/java/com/taobao/arthas/core/shell/command/CommandResolver.java new file mode 100644 index 00000000..ce99daca --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/command/CommandResolver.java @@ -0,0 +1,15 @@ +package com.taobao.arthas.core.shell.command; + +import java.util.List; + +/** + * A resolver for commands, so the shell can discover commands. + * + * @author Julien Viet + */ +public interface CommandResolver { + /** + * @return the current commands + */ + List commands(); +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/command/impl/AnnotatedCommandImpl.java b/core/src/main/java/com/taobao/arthas/core/shell/command/impl/AnnotatedCommandImpl.java new file mode 100644 index 00000000..ea5fb391 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/command/impl/AnnotatedCommandImpl.java @@ -0,0 +1,115 @@ +package com.taobao.arthas.core.shell.command.impl; + +import com.taobao.arthas.core.shell.cli.Completion; +import com.taobao.arthas.core.shell.command.AnnotatedCommand; +import com.taobao.arthas.core.shell.command.Command; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.shell.handlers.Handler; +import com.taobao.arthas.core.util.UserStatUtil; +import com.taobao.middleware.cli.CLI; +import com.taobao.middleware.cli.Option; +import com.taobao.middleware.cli.annotations.CLIConfigurator; + +import java.util.Collections; + +/** + * @author beiwei30 on 10/11/2016. + */ +public class AnnotatedCommandImpl extends Command { + + private CLI cli; + private Class clazz; + private Handler processHandler = new ProcessHandler(); + + public AnnotatedCommandImpl(Class clazz) { + this.clazz = clazz; + cli = CLIConfigurator.define(clazz); + cli.addOption(new Option().setArgName("help").setFlag(true).setShortName("h").setLongName("help") + .setDescription("this help").setHelp(true)); + } + + private boolean shouldOverridesName(Class clazz) { + try { + clazz.getDeclaredMethod("name"); + return true; + } catch (NoSuchMethodException ignore) { + return false; + } + } + + private boolean shouldOverrideCli(Class clazz) { + try { + clazz.getDeclaredMethod("cli"); + return true; + } catch (NoSuchMethodException ignore) { + return false; + } + } + + @Override + public String name() { + if (shouldOverridesName(clazz)) { + try { + return clazz.newInstance().name(); + } catch (Exception ignore) { + // Use cli.getName() instead + } + } + return cli.getName(); + } + + @Override + public CLI cli() { + if (shouldOverrideCli(clazz)) { + try { + return clazz.newInstance().cli(); + } catch (Exception ignore) { + // Use cli instead + } + } + return cli; + } + + private void process(CommandProcess process) { + AnnotatedCommand instance; + try { + instance = clazz.newInstance(); + } catch (Exception e) { + process.end(); + return; + } + CLIConfigurator.inject(process.commandLine(), instance); + instance.process(process); + UserStatUtil.arthasUsageSuccess(name(), process.args()); + } + + @Override + public Handler processHandler() { + return processHandler; + } + + @Override + public void complete(final Completion completion) { + final AnnotatedCommand instance; + try { + instance = clazz.newInstance(); + } catch (Exception e) { + super.complete(completion); + return; + } + + try { + instance.complete(completion); + } catch (Throwable t) { + completion.complete(Collections.emptyList()); + } + } + + private class ProcessHandler implements Handler { + @Override + public void handle(CommandProcess process) { + process(process); + } + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/command/impl/CommandBuilderImpl.java b/core/src/main/java/com/taobao/arthas/core/shell/command/impl/CommandBuilderImpl.java new file mode 100644 index 00000000..e3fdfbda --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/command/impl/CommandBuilderImpl.java @@ -0,0 +1,73 @@ +package com.taobao.arthas.core.shell.command.impl; + +import com.taobao.arthas.core.shell.cli.Completion; +import com.taobao.arthas.core.shell.command.Command; +import com.taobao.arthas.core.shell.command.CommandBuilder; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.shell.handlers.Handler; +import com.taobao.middleware.cli.CLI; + +import java.util.Collections; + +/** + * @author Julien Viet + */ +public class CommandBuilderImpl extends CommandBuilder { + + private final String name; + private final CLI cli; + private Handler processHandler; + private Handler completeHandler; + + public CommandBuilderImpl(String name, CLI cli) { + this.name = name; + this.cli = cli; + } + + @Override + public CommandBuilderImpl processHandler(Handler handler) { + processHandler = handler; + return this; + } + + @Override + public CommandBuilderImpl completionHandler(Handler handler) { + completeHandler = handler; + return this; + } + + @Override + public Command build() { + return new CommandImpl(); + } + + private class CommandImpl extends Command { + @Override + public String name() { + return name; + } + + @Override + public CLI cli() { + return cli; + } + + @Override + public Handler processHandler() { + return processHandler; + } + + @Override + public void complete(final Completion completion) { + if (completeHandler != null) { + try { + completeHandler.handle(completion); + } catch (Throwable t) { + completion.complete(Collections.emptyList()); + } + } else { + super.complete(completion); + } + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/command/internal/CloseFunction.java b/core/src/main/java/com/taobao/arthas/core/shell/command/internal/CloseFunction.java new file mode 100644 index 00000000..3f3a5add --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/command/internal/CloseFunction.java @@ -0,0 +1,11 @@ +package com.taobao.arthas.core.shell.command.internal; + +import io.termd.core.function.Function; + +/** + * @author diecui1202 on 2017/11/2. + */ +public interface CloseFunction extends Function { + + void close(); +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/command/internal/GrepHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/command/internal/GrepHandler.java new file mode 100644 index 00000000..460bd25b --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/command/internal/GrepHandler.java @@ -0,0 +1,52 @@ +package com.taobao.arthas.core.shell.command.internal; + +import com.taobao.arthas.core.shell.cli.CliToken; +import com.taobao.middleware.cli.Argument; +import com.taobao.middleware.cli.CLIs; +import com.taobao.middleware.cli.CommandLine; +import com.taobao.middleware.cli.Option; + +import java.util.List; + +/** + * @author beiwei30 on 12/12/2016. + */ +public class GrepHandler extends StdoutHandler { + public static final String NAME = "grep"; + + private String keyword; + private boolean ignoreCase; + + public static StdoutHandler inject(List tokens) { + List args = StdoutHandler.parseArgs(tokens, NAME); + CommandLine commandLine = CLIs.create(NAME) + .addOption(new Option().setShortName("i").setLongName("ignore-case").setFlag(true)) + .addArgument(new Argument().setArgName("keyword").setIndex(0)) + .parse(args); + Boolean ignoreCase = commandLine.isFlagEnabled("ignore-case"); + String keyword = commandLine.getArgumentValue(0); + return new GrepHandler(keyword, ignoreCase); + } + + private GrepHandler(String keyword, boolean ignoreCase) { + this.keyword = keyword; + this.ignoreCase = ignoreCase; + } + + @Override + public String apply(String input) { + StringBuilder output = new StringBuilder(); + String[] lines = input.split("\n"); + for (String line : lines) { + if (ignoreCase) { + line = line.toLowerCase(); + keyword = keyword.toLowerCase(); + } + + if (line.contains(keyword)) { + output.append(line).append("\n"); + } + } + return output.toString(); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/command/internal/PlainTextHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/command/internal/PlainTextHandler.java new file mode 100644 index 00000000..7faccecd --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/command/internal/PlainTextHandler.java @@ -0,0 +1,22 @@ +package com.taobao.arthas.core.shell.command.internal; + +import com.taobao.arthas.core.shell.cli.CliToken; +import com.taobao.text.util.RenderUtil; + +import java.util.List; + +/** + * @author beiwei30 on 20/12/2016. + */ +public class PlainTextHandler extends StdoutHandler { + public static String NAME = "plaintext"; + + public static StdoutHandler inject(List tokens) { + return new PlainTextHandler(); + } + + @Override + public String apply(String s) { + return RenderUtil.ansiToPlainText(s); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/command/internal/RedirectHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/command/internal/RedirectHandler.java new file mode 100644 index 00000000..cfcaa952 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/command/internal/RedirectHandler.java @@ -0,0 +1,43 @@ +package com.taobao.arthas.core.shell.command.internal; + +import com.taobao.arthas.core.util.LogUtil; +import com.taobao.middleware.logger.LoggerFactory; +import org.slf4j.Logger; + +/** + * 重定向处理类 + * + * @author gehui 2017年7月27日 上午11:38:40 + */ +public class RedirectHandler extends PlainTextHandler implements CloseFunction { + + private Logger logger = null; + + public RedirectHandler() { + + } + + public RedirectHandler(String name) { + com.taobao.middleware.logger.Logger log = LoggerFactory.getLogger(name); + log.activateAppenderWithSizeRolling("arthas-cache", name, "UTF-8", "200MB", 3); + log.setAdditivity(false); + log.activateAsync(128, -1); + logger = (Logger) log.getDelegate(); + } + + @Override + public String apply(String data) { + data = super.apply(data); + if (logger != null) { + logger.info(data); + } else { + LogUtil.getResultLogger().info(data); + } + return data; + } + + @Override + public void close() { + LogUtil.closeSlf4jLogger(logger); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/command/internal/StatisticsFunction.java b/core/src/main/java/com/taobao/arthas/core/shell/command/internal/StatisticsFunction.java new file mode 100644 index 00000000..f6990aeb --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/command/internal/StatisticsFunction.java @@ -0,0 +1,13 @@ +package com.taobao.arthas.core.shell.command.internal; + +import io.termd.core.function.Function; + +/** + * 统计类Function的接口 + * + * @author diecui1202 on 2017/10/24. + */ +public interface StatisticsFunction extends Function { + + String result(); +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/command/internal/StdoutHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/command/internal/StdoutHandler.java new file mode 100644 index 00000000..3939b8ad --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/command/internal/StdoutHandler.java @@ -0,0 +1,55 @@ +package com.taobao.arthas.core.shell.command.internal; + +import com.taobao.arthas.core.shell.cli.CliToken; +import io.termd.core.function.Function; + +import java.util.LinkedList; +import java.util.List; + +/** + * @author beiwei30 on 20/12/2016. + */ +public abstract class StdoutHandler implements Function { + + public static StdoutHandler inject(List tokens) { + CliToken firstTextToken = null; + for (CliToken token : tokens) { + if (token.isText()) { + firstTextToken = token; + break; + } + } + + if (firstTextToken == null) { + return null; + } + + if (firstTextToken.value().equals(GrepHandler.NAME)) { + return GrepHandler.inject(tokens); + } else if (firstTextToken.value().equals(PlainTextHandler.NAME)) { + return PlainTextHandler.inject(tokens); + } else if (firstTextToken.value().equals(WordCountHandler.NAME)) { + return WordCountHandler.inject(tokens); + } else { + return null; + } + } + + public static List parseArgs(List tokens, String command) { + List args = new LinkedList(); + boolean found = false; + for (CliToken token : tokens) { + if (token.isText() && token.value().equals(command)) { + found = true; + } else if (token.isText() && found) { + args.add(token.value()); + } + } + return args; + } + + @Override + public String apply(String s) { + return s; + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/command/internal/TermHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/command/internal/TermHandler.java new file mode 100644 index 00000000..a8b3dae2 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/command/internal/TermHandler.java @@ -0,0 +1,22 @@ +package com.taobao.arthas.core.shell.command.internal; + +import com.taobao.arthas.core.shell.term.Term; + +/** + * 将数据写到term + * + * @author gehui 2017年7月26日 上午11:20:00 + */ +public class TermHandler extends StdoutHandler { + private Term term; + + public TermHandler(Term term) { + this.term = term; + } + + @Override + public String apply(String data) { + term.write(data); + return data; + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/command/internal/WordCountHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/command/internal/WordCountHandler.java new file mode 100644 index 00000000..640de22b --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/command/internal/WordCountHandler.java @@ -0,0 +1,57 @@ +package com.taobao.arthas.core.shell.command.internal; + +import com.taobao.arthas.core.shell.cli.CliToken; +import com.taobao.middleware.cli.CLIs; +import com.taobao.middleware.cli.CommandLine; +import com.taobao.middleware.cli.Option; + +import java.util.List; + +/** + * @author ralf0131 2017-02-23 23:28. + */ +public class WordCountHandler extends StdoutHandler implements StatisticsFunction { + + public static final String NAME = "wc"; + + private boolean lineMode; + + private String result = null; + private volatile int total = 0; + + public static StdoutHandler inject(List tokens) { + List args = StdoutHandler.parseArgs(tokens, NAME); + CommandLine commandLine = CLIs.create(NAME) + .addOption(new Option().setShortName("l").setFlag(true)) + .parse(args); + Boolean lineMode = commandLine.isFlagEnabled("l"); + return new WordCountHandler(lineMode); + } + + private WordCountHandler(boolean lineMode) { + this.lineMode = lineMode; + } + + @Override + public String apply(String input) { + if (!this.lineMode) { + // TODO the default behavior should be equivalent to `wc -l -w -c` + result = "wc currently only support wc -l!\n"; + } else { + if (input != null && !"".equals(input.trim())) { + total += input.split("\n").length; + } + } + + return null; + } + + @Override + public String result() { + if (result != null) { + return result; + } + + return total + "\n"; + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/future/Future.java b/core/src/main/java/com/taobao/arthas/core/shell/future/Future.java new file mode 100644 index 00000000..772ce1e8 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/future/Future.java @@ -0,0 +1,121 @@ +package com.taobao.arthas.core.shell.future; + + +import com.taobao.arthas.core.shell.handlers.Handler; + +public class Future { + private boolean failed; + private boolean succeeded; + private Handler> handler; + private T result; + private Throwable throwable; + + public Future() { + } + + public Future(Throwable t) { + fail(t); + } + + public Future(String failureMessage) { + this(new Throwable(failureMessage)); + } + + public Future(T result) { + complete(result); + } + + public static Future future() { + return new Future(); + } + + public static Future succeededFuture() { + return new Future((T) null); + } + + public static Future succeededFuture(T result) { + return new Future(result); + } + + public static Future failedFuture(Throwable t) { + return new Future(t); + } + + public static Future failedFuture(String failureMessage) { + return new Future(failureMessage); + } + + public boolean isComplete() { + return failed || succeeded; + } + + public Future setHandler(Handler> handler) { + this.handler = handler; + checkCallHandler(); + return this; + } + + + public void complete(T result) { + checkComplete(); + this.result = result; + succeeded = true; + checkCallHandler(); + } + + public void complete() { + complete(null); + } + + public void fail(Throwable throwable) { + checkComplete(); + this.throwable = throwable; + failed = true; + checkCallHandler(); + } + + public void fail(String failureMessage) { + fail(new Throwable(failureMessage)); + } + + public T result() { + return result; + } + + public Throwable cause() { + return throwable; + } + + public boolean succeeded() { + return succeeded; + } + + public boolean failed() { + return failed; + } + + public Handler> completer() { + return new Handler>() { + @Override + public void handle(Future ar) { + if (ar.succeeded()) { + complete(ar.result()); + } else { + fail(ar.cause()); + } + } + }; + } + + private void checkCallHandler() { + if (handler != null && isComplete()) { + handler.handle(this); + } + } + + private void checkComplete() { + if (succeeded || failed) { + throw new IllegalStateException("Result is already complete: " + (succeeded ? "succeeded" : "failed")); + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/handlers/BindHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/handlers/BindHandler.java new file mode 100644 index 00000000..36cc31c6 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/handlers/BindHandler.java @@ -0,0 +1,29 @@ +package com.taobao.arthas.core.shell.handlers; + +import com.taobao.arthas.core.shell.future.Future; +import com.taobao.arthas.core.util.LogUtil; +import com.taobao.middleware.logger.Logger; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * @author ralf0131 2017-04-24 18:23. + */ +public class BindHandler implements Handler> { + + private static final Logger logger = LogUtil.getArthasLogger(); + + private AtomicBoolean isBindRef; + + public BindHandler(AtomicBoolean isBindRef) { + this.isBindRef = isBindRef; + } + + @Override + public void handle(Future event) { + if (event.failed()) { + logger.error(null, "Error listening term server:", event.cause()); + isBindRef.compareAndSet(true, false); + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/handlers/Handler.java b/core/src/main/java/com/taobao/arthas/core/shell/handlers/Handler.java new file mode 100644 index 00000000..98745049 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/handlers/Handler.java @@ -0,0 +1,10 @@ +package com.taobao.arthas.core.shell.handlers; + +public interface Handler { + /** + * Something has happened, so handle it. + * + * @param event the event to handle + */ + void handle(E event); +} \ No newline at end of file diff --git a/core/src/main/java/com/taobao/arthas/core/shell/handlers/NoOpHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/handlers/NoOpHandler.java new file mode 100644 index 00000000..74e36370 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/handlers/NoOpHandler.java @@ -0,0 +1,20 @@ +package com.taobao.arthas.core.shell.handlers; + +import com.taobao.arthas.core.shell.future.Future; +import com.taobao.arthas.core.util.LogUtil; +import com.taobao.middleware.logger.Logger; + +/** + * @author beiwei30 on 22/11/2016. + */ +public class NoOpHandler implements Handler { + + private static final Logger logger = LogUtil.getArthasLogger(); + + @Override + public void handle(Object event) { + if (event instanceof Future && ((Future) event).failed()) { + logger.error(null, "Error listening term server:", ((Future) event).cause()); + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/handlers/command/CommandInterruptHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/handlers/command/CommandInterruptHandler.java new file mode 100644 index 00000000..6fcd1fda --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/handlers/command/CommandInterruptHandler.java @@ -0,0 +1,22 @@ +package com.taobao.arthas.core.shell.handlers.command; + +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.shell.handlers.Handler; + +/** + * @author ralf0131 2017-01-09 13:23. + */ +public class CommandInterruptHandler implements Handler { + + private CommandProcess process; + + public CommandInterruptHandler(CommandProcess process) { + this.process = process; + } + + @Override + public void handle(Void event) { + process.end(); + process.session().unLock(); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/handlers/server/SessionClosedHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/handlers/server/SessionClosedHandler.java new file mode 100644 index 00000000..2be82a90 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/handlers/server/SessionClosedHandler.java @@ -0,0 +1,24 @@ +package com.taobao.arthas.core.shell.handlers.server; + +import com.taobao.arthas.core.shell.future.Future; +import com.taobao.arthas.core.shell.handlers.Handler; +import com.taobao.arthas.core.shell.impl.ShellImpl; +import com.taobao.arthas.core.shell.impl.ShellServerImpl; + +/** + * @author beiwei30 on 23/11/2016. + */ +public class SessionClosedHandler implements Handler> { + private ShellServerImpl shellServer; + private final ShellImpl session; + + public SessionClosedHandler(ShellServerImpl shellServer, ShellImpl session) { + this.shellServer = shellServer; + this.session = session; + } + + @Override + public void handle(Future ar) { + shellServer.removeSession(session); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/handlers/server/SessionsClosedHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/handlers/server/SessionsClosedHandler.java new file mode 100644 index 00000000..6f7a0657 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/handlers/server/SessionsClosedHandler.java @@ -0,0 +1,26 @@ +package com.taobao.arthas.core.shell.handlers.server; + +import com.taobao.arthas.core.shell.future.Future; +import com.taobao.arthas.core.shell.handlers.Handler; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author beiwei30 on 23/11/2016. + */ +public class SessionsClosedHandler implements Handler> { + private final AtomicInteger count; + private final Handler> completionHandler; + + public SessionsClosedHandler(AtomicInteger count, Handler> completionHandler) { + this.count = count; + this.completionHandler = completionHandler; + } + + @Override + public void handle(Future event) { + if (count.decrementAndGet() == 0) { + completionHandler.handle(Future.succeededFuture()); + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/handlers/server/TermServerListenHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/handlers/server/TermServerListenHandler.java new file mode 100644 index 00000000..7fbe0858 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/handlers/server/TermServerListenHandler.java @@ -0,0 +1,49 @@ +package com.taobao.arthas.core.shell.handlers.server; + +import com.taobao.arthas.core.shell.future.Future; +import com.taobao.arthas.core.shell.handlers.Handler; +import com.taobao.arthas.core.shell.impl.ShellServerImpl; +import com.taobao.arthas.core.shell.term.TermServer; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author beiwei30 on 23/11/2016. + */ +public class TermServerListenHandler implements Handler> { + private ShellServerImpl shellServer; + private Handler> listenHandler; + private List toStart; + private AtomicInteger count; + private AtomicBoolean failed; + + public TermServerListenHandler(ShellServerImpl shellServer, Handler> listenHandler, List toStart) { + this.shellServer = shellServer; + this.listenHandler = listenHandler; + this.toStart = toStart; + this.count = new AtomicInteger(toStart.size()); + this.failed = new AtomicBoolean(); + } + + @Override + public void handle(Future ar) { + if (ar.failed()) { + failed.set(true); + } + + if (count.decrementAndGet() == 0) { + if (failed.get()) { + listenHandler.handle(Future.failedFuture(ar.cause())); + for (TermServer termServer : toStart) { + termServer.close(); + } + } else { + shellServer.setClosed(false); + shellServer.setTimer(); + listenHandler.handle(Future.succeededFuture()); + } + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/handlers/server/TermServerTermHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/handlers/server/TermServerTermHandler.java new file mode 100644 index 00000000..b0ab60af --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/handlers/server/TermServerTermHandler.java @@ -0,0 +1,21 @@ +package com.taobao.arthas.core.shell.handlers.server; + +import com.taobao.arthas.core.shell.handlers.Handler; +import com.taobao.arthas.core.shell.impl.ShellServerImpl; +import com.taobao.arthas.core.shell.term.Term; + +/** + * @author beiwei30 on 23/11/2016. + */ +public class TermServerTermHandler implements Handler { + private ShellServerImpl shellServer; + + public TermServerTermHandler(ShellServerImpl shellServer) { + this.shellServer = shellServer; + } + + @Override + public void handle(Term term) { + shellServer.handleTerm(term); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/handlers/shell/CloseHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/handlers/shell/CloseHandler.java new file mode 100644 index 00000000..e33f803e --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/handlers/shell/CloseHandler.java @@ -0,0 +1,20 @@ +package com.taobao.arthas.core.shell.handlers.shell; + +import com.taobao.arthas.core.shell.handlers.Handler; +import com.taobao.arthas.core.shell.impl.ShellImpl; + +/** + * @author beiwei30 on 23/11/2016. + */ +public class CloseHandler implements Handler { + private ShellImpl shell; + + public CloseHandler(ShellImpl shell) { + this.shell = shell; + } + + @Override + public void handle(Void event) { + shell.jobController().close(shell.closedFutureHandler()); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/handlers/shell/CommandManagerCompletionHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/handlers/shell/CommandManagerCompletionHandler.java new file mode 100644 index 00000000..09fe4d07 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/handlers/shell/CommandManagerCompletionHandler.java @@ -0,0 +1,21 @@ +package com.taobao.arthas.core.shell.handlers.shell; + +import com.taobao.arthas.core.shell.cli.Completion; +import com.taobao.arthas.core.shell.handlers.Handler; +import com.taobao.arthas.core.shell.system.impl.InternalCommandManager; + +/** + * @author beiwei30 on 23/11/2016. + */ +public class CommandManagerCompletionHandler implements Handler { + private InternalCommandManager commandManager; + + public CommandManagerCompletionHandler(InternalCommandManager commandManager) { + this.commandManager = commandManager; + } + + @Override + public void handle(Completion completion) { + commandManager.complete(completion); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/handlers/shell/FutureHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/handlers/shell/FutureHandler.java new file mode 100644 index 00000000..bcaa71cd --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/handlers/shell/FutureHandler.java @@ -0,0 +1,20 @@ +package com.taobao.arthas.core.shell.handlers.shell; + +import com.taobao.arthas.core.shell.future.Future; +import com.taobao.arthas.core.shell.handlers.Handler; + +/** + * @author beiwei30 on 23/11/2016. + */ +public class FutureHandler implements Handler { + private Future future; + + public FutureHandler(Future future) { + this.future = future; + } + + @Override + public void handle(Void event) { + future.complete(); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/handlers/shell/InterruptHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/handlers/shell/InterruptHandler.java new file mode 100644 index 00000000..a7db4c4e --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/handlers/shell/InterruptHandler.java @@ -0,0 +1,24 @@ +package com.taobao.arthas.core.shell.handlers.shell; + +import com.taobao.arthas.core.shell.impl.ShellImpl; +import com.taobao.arthas.core.shell.term.SignalHandler; + +/** + * @author beiwei30 on 23/11/2016. + */ +public class InterruptHandler implements SignalHandler { + + private ShellImpl shell; + + public InterruptHandler(ShellImpl shell) { + this.shell = shell; + } + + @Override + public boolean deliver(int key) { + if (shell.getForegroundJob() != null) { + return shell.getForegroundJob().interrupt(); + } + return true; + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/handlers/shell/ShellForegroundUpdateHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/handlers/shell/ShellForegroundUpdateHandler.java new file mode 100644 index 00000000..a7f6852f --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/handlers/shell/ShellForegroundUpdateHandler.java @@ -0,0 +1,23 @@ +package com.taobao.arthas.core.shell.handlers.shell; + +import com.taobao.arthas.core.shell.handlers.Handler; +import com.taobao.arthas.core.shell.impl.ShellImpl; +import com.taobao.arthas.core.shell.system.Job; + +/** + * @author beiwei30 on 23/11/2016. + */ +public class ShellForegroundUpdateHandler implements Handler { + private ShellImpl shell; + + public ShellForegroundUpdateHandler(ShellImpl shell) { + this.shell = shell; + } + + @Override + public void handle(Job job) { + if (job == null) { + shell.readline(); + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/handlers/shell/ShellLineHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/handlers/shell/ShellLineHandler.java new file mode 100644 index 00000000..275b9c4f --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/handlers/shell/ShellLineHandler.java @@ -0,0 +1,171 @@ +package com.taobao.arthas.core.shell.handlers.shell; + +import com.taobao.arthas.core.shell.cli.CliToken; +import com.taobao.arthas.core.shell.cli.CliTokens; +import com.taobao.arthas.core.shell.handlers.Handler; +import com.taobao.arthas.core.shell.impl.ShellImpl; +import com.taobao.arthas.core.shell.system.ExecStatus; +import com.taobao.arthas.core.shell.system.Job; +import com.taobao.arthas.core.shell.term.Term; +import com.taobao.arthas.core.util.TokenUtils; + +import java.util.List; + +/** + * @author beiwei30 on 23/11/2016. + */ +public class ShellLineHandler implements Handler { + + private ShellImpl shell; + private Term term; + + public ShellLineHandler(ShellImpl shell) { + this.shell = shell; + this.term = shell.term(); + } + + @Override + public void handle(String line) { + if (line == null) { + // EOF + handleExit(); + return; + } + + List tokens = CliTokens.tokenize(line); + CliToken first = TokenUtils.findFirstTextToken(tokens); + if (first == null) { + // For now do like this + shell.readline(); + return; + } + + String name = first.value(); + if (name.equals("exit") || name.equals("logout") || name.equals("quit")) { + handleExit(); + return; + } else if (name.equals("jobs")) { + handleJobs(); + return; + } else if (name.equals("fg")) { + handleForeground(tokens); + return; + } else if (name.equals("bg")) { + handleBackground(tokens); + return; + } else if (name.equals("kill")) { + handleKill(tokens); + return; + } + + Job job = createJob(tokens); + if (job != null) { + job.run(); + } + } + + private int getJobId(String arg) { + int result = -1; + try { + if (arg.startsWith("%")) { + result = Integer.parseInt(arg.substring(1)); + } else { + result = Integer.parseInt(arg); + } + } catch (Exception e) { + } + return result; + } + + private Job createJob(List tokens) { + Job job; + try { + job = shell.createJob(tokens); + } catch (Exception e) { + term.echo(e.getMessage() + "\n"); + shell.readline(); + return null; + } + return job; + } + + private void handleKill(List tokens) { + String arg = TokenUtils.findSecondTokenText(tokens); + if (arg == null) { + term.write("kill: usage: kill job_id\n"); + shell.readline(); + return; + } + Job job = shell.jobController().getJob(getJobId(arg)); + if (job == null) { + term.write(arg + " : no such job\n"); + shell.readline(); + } else { + job.terminate(); + term.write("kill job " + job.id() + " success\n"); + shell.readline(); + } + } + + private void handleBackground(List tokens) { + String arg = TokenUtils.findSecondTokenText(tokens); + Job job; + if (arg == null) { + job = shell.getForegroundJob(); + } else { + job = shell.jobController().getJob(getJobId(arg)); + } + if (job == null) { + term.write(arg + " : no such job\n"); + shell.readline(); + } else { + if (job.status() == ExecStatus.STOPPED) { + job.resume(false); + term.echo(shell.statusLine(job, ExecStatus.RUNNING)); + shell.readline(); + } else { + term.write("job " + job.id() + " is already running\n"); + shell.readline(); + } + } + } + + private void handleForeground(List tokens) { + String arg = TokenUtils.findSecondTokenText(tokens); + Job job; + if (arg == null) { + job = shell.getForegroundJob(); + } else { + job = shell.jobController().getJob(getJobId(arg)); + } + if (job == null) { + term.write(arg + " : no such job\n"); + shell.readline(); + } else { + if (job.getSession() != shell.session()) { + term.write("job " + job.id() + " doesn't belong to this session, so can not fg it\n"); + shell.readline(); + } else if (job.status() == ExecStatus.STOPPED) { + job.resume(true); + } else if (job.status() == ExecStatus.RUNNING) { + // job is running + job.toForeground(); + } else { + term.write("job " + job.id() + " is already terminated, so can not fg it\n"); + shell.readline(); + } + } + } + + private void handleJobs() { + for (Job job : shell.jobController().jobs()) { + String statusLine = shell.statusLine(job, job.status()); + term.write(statusLine); + } + shell.readline(); + } + + private void handleExit() { + term.close(); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/handlers/shell/SuspendHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/handlers/shell/SuspendHandler.java new file mode 100644 index 00000000..773b487c --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/handlers/shell/SuspendHandler.java @@ -0,0 +1,36 @@ +package com.taobao.arthas.core.shell.handlers.shell; + +import com.taobao.arthas.core.shell.impl.ShellImpl; +import com.taobao.arthas.core.shell.system.ExecStatus; +import com.taobao.arthas.core.shell.system.Job; +import com.taobao.arthas.core.shell.system.JobController; +import com.taobao.arthas.core.shell.term.SignalHandler; +import com.taobao.arthas.core.shell.term.Term; +import io.termd.core.util.Helper; + +import java.util.Set; + +/** + * @author beiwei30 on 23/11/2016. + */ +public class SuspendHandler implements SignalHandler { + + private ShellImpl shell; + + public SuspendHandler(ShellImpl shell) { + this.shell = shell; + } + + @Override + public boolean deliver(int key) { + Term term = shell.term(); + + Job job = shell.getForegroundJob(); + if (job != null) { + term.echo(shell.statusLine(job, ExecStatus.STOPPED)); + job.suspend(); + } + + return true; + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/handlers/term/CloseHandlerWrapper.java b/core/src/main/java/com/taobao/arthas/core/shell/handlers/term/CloseHandlerWrapper.java new file mode 100644 index 00000000..6a9bde20 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/handlers/term/CloseHandlerWrapper.java @@ -0,0 +1,20 @@ +package com.taobao.arthas.core.shell.handlers.term; + +import com.taobao.arthas.core.shell.handlers.Handler; +import io.termd.core.function.Consumer; + +/** + * @author beiwei30 on 22/11/2016. + */ +public class CloseHandlerWrapper implements Consumer { + private final Handler handler; + + public CloseHandlerWrapper(Handler handler) { + this.handler = handler; + } + + @Override + public void accept(Void v) { + handler.handle(v); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/handlers/term/DefaultTermStdinHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/handlers/term/DefaultTermStdinHandler.java new file mode 100644 index 00000000..3742d92b --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/handlers/term/DefaultTermStdinHandler.java @@ -0,0 +1,22 @@ +package com.taobao.arthas.core.shell.handlers.term; + +import com.taobao.arthas.core.shell.term.impl.TermImpl; +import io.termd.core.function.Consumer; + +/** + * @author beiwei30 on 23/11/2016. + */ +public class DefaultTermStdinHandler implements Consumer { + private TermImpl term; + + public DefaultTermStdinHandler(TermImpl term) { + this.term = term; + } + + @Override + public void accept(int[] codePoints) { + // Echo + term.echo(codePoints); + term.getReadline().queueEvent(codePoints); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/handlers/term/EventHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/handlers/term/EventHandler.java new file mode 100644 index 00000000..b2d756b1 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/handlers/term/EventHandler.java @@ -0,0 +1,31 @@ +package com.taobao.arthas.core.shell.handlers.term; + +import com.taobao.arthas.core.shell.term.impl.TermImpl; +import io.termd.core.function.BiConsumer; +import io.termd.core.tty.TtyEvent; + +/** + * @author beiwei30 on 23/11/2016. + */ +public class EventHandler implements BiConsumer { + private TermImpl term; + + public EventHandler(TermImpl term) { + this.term = term; + } + + @Override + public void accept(TtyEvent event, Integer key) { + switch (event) { + case INTR: + term.handleIntr(key); + break; + case EOF: + term.handleEof(key); + break; + case SUSP: + term.handleSusp(key); + break; + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/handlers/term/RequestHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/handlers/term/RequestHandler.java new file mode 100644 index 00000000..4d0478a3 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/handlers/term/RequestHandler.java @@ -0,0 +1,24 @@ +package com.taobao.arthas.core.shell.handlers.term; + +import com.taobao.arthas.core.shell.handlers.Handler; +import com.taobao.arthas.core.shell.term.impl.TermImpl; +import io.termd.core.function.Consumer; + +/** + * @author beiwei30 on 23/11/2016. + */ +public class RequestHandler implements Consumer { + private TermImpl term; + private final Handler lineHandler; + + public RequestHandler(TermImpl term, Handler lineHandler) { + this.term = term; + this.lineHandler = lineHandler; + } + + @Override + public void accept(String line) { + term.setInReadline(false); + lineHandler.handle(line); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/handlers/term/SizeHandlerWrapper.java b/core/src/main/java/com/taobao/arthas/core/shell/handlers/term/SizeHandlerWrapper.java new file mode 100644 index 00000000..cd774a0e --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/handlers/term/SizeHandlerWrapper.java @@ -0,0 +1,21 @@ +package com.taobao.arthas.core.shell.handlers.term; + +import com.taobao.arthas.core.shell.handlers.Handler; +import io.termd.core.function.Consumer; +import io.termd.core.util.Vector; + +/** + * @author beiwei30 on 22/11/2016. + */ +public class SizeHandlerWrapper implements Consumer { + private final Handler handler; + + public SizeHandlerWrapper(Handler handler) { + this.handler = handler; + } + + @Override + public void accept(Vector resize) { + handler.handle(null); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/handlers/term/StdinHandlerWrapper.java b/core/src/main/java/com/taobao/arthas/core/shell/handlers/term/StdinHandlerWrapper.java new file mode 100644 index 00000000..761f31cd --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/handlers/term/StdinHandlerWrapper.java @@ -0,0 +1,21 @@ +package com.taobao.arthas.core.shell.handlers.term; + +import com.taobao.arthas.core.shell.handlers.Handler; +import io.termd.core.function.Consumer; +import io.termd.core.util.Helper; + +/** + * @author beiwei30 on 22/11/2016. + */ +public class StdinHandlerWrapper implements Consumer { + private final Handler handler; + + public StdinHandlerWrapper(Handler handler) { + this.handler = handler; + } + + @Override + public void accept(int[] codePoints) { + handler.handle(Helper.fromCodePoints(codePoints)); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/impl/BuiltinCommandResolver.java b/core/src/main/java/com/taobao/arthas/core/shell/impl/BuiltinCommandResolver.java new file mode 100644 index 00000000..ae497c5c --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/impl/BuiltinCommandResolver.java @@ -0,0 +1,39 @@ +package com.taobao.arthas.core.shell.impl; + +import com.taobao.arthas.core.shell.command.Command; +import com.taobao.arthas.core.shell.command.CommandBuilder; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.shell.command.CommandResolver; +import com.taobao.arthas.core.shell.command.internal.GrepHandler; +import com.taobao.arthas.core.shell.command.internal.PlainTextHandler; +import com.taobao.arthas.core.shell.command.internal.WordCountHandler; +import com.taobao.arthas.core.shell.handlers.Handler; +import com.taobao.arthas.core.shell.handlers.NoOpHandler; + +import java.util.Arrays; +import java.util.List; + +/** + * @author beiwei30 on 23/11/2016. + */ +class BuiltinCommandResolver implements CommandResolver { + + private Handler handler; + + public BuiltinCommandResolver() { + this.handler = new NoOpHandler(); + } + + @Override + public List commands() { + return Arrays.asList(CommandBuilder.command("exit").processHandler(handler).build(), + CommandBuilder.command("quit").processHandler(handler).build(), + CommandBuilder.command("jobs").processHandler(handler).build(), + CommandBuilder.command("fg").processHandler(handler).build(), + CommandBuilder.command("bg").processHandler(handler).build(), + CommandBuilder.command("kill").processHandler(handler).build(), + CommandBuilder.command(PlainTextHandler.NAME).processHandler(handler).build(), + CommandBuilder.command(GrepHandler.NAME).processHandler(handler).build(), + CommandBuilder.command(WordCountHandler.NAME).processHandler(handler).build()); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/impl/ShellImpl.java b/core/src/main/java/com/taobao/arthas/core/shell/impl/ShellImpl.java new file mode 100644 index 00000000..f7b318f1 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/impl/ShellImpl.java @@ -0,0 +1,179 @@ +package com.taobao.arthas.core.shell.impl; + +import com.taobao.arthas.core.shell.Shell; +import com.taobao.arthas.core.shell.ShellServer; +import com.taobao.arthas.core.shell.cli.CliToken; +import com.taobao.arthas.core.shell.cli.CliTokens; +import com.taobao.arthas.core.shell.future.Future; +import com.taobao.arthas.core.shell.handlers.shell.CloseHandler; +import com.taobao.arthas.core.shell.handlers.shell.CommandManagerCompletionHandler; +import com.taobao.arthas.core.shell.handlers.shell.FutureHandler; +import com.taobao.arthas.core.shell.handlers.shell.InterruptHandler; +import com.taobao.arthas.core.shell.handlers.shell.ShellLineHandler; +import com.taobao.arthas.core.shell.handlers.shell.SuspendHandler; +import com.taobao.arthas.core.shell.session.Session; +import com.taobao.arthas.core.shell.session.impl.SessionImpl; +import com.taobao.arthas.core.shell.system.ExecStatus; +import com.taobao.arthas.core.shell.system.Job; +import com.taobao.arthas.core.shell.system.JobController; +import com.taobao.arthas.core.shell.system.impl.InternalCommandManager; +import com.taobao.arthas.core.shell.system.impl.JobControllerImpl; +import com.taobao.arthas.core.shell.system.impl.JobImpl; +import com.taobao.arthas.core.shell.term.Term; +import com.taobao.arthas.core.util.Constants; +import com.taobao.arthas.core.util.LogUtil; +import com.taobao.middleware.logger.Logger; + +import java.lang.instrument.Instrumentation; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.UUID; + +/** + * The shell session as seen from the shell server perspective. + * + * @author Julien Viet + */ +public class ShellImpl implements Shell { + + private static final Logger logger = LogUtil.getArthasLogger(); + + private JobControllerImpl jobController; + final String id; + final Future closedFuture; + private InternalCommandManager commandManager; + private Session session = new SessionImpl(); + private Term term; + private String welcome; + private Job currentForegroundJob; + + public ShellImpl(ShellServer server, Term term, InternalCommandManager commandManager, + Instrumentation instrumentation, int pid, JobControllerImpl jobController) { + session.put(Session.COMMAND_MANAGER, commandManager); + session.put(Session.INSTRUMENTATION, instrumentation); + session.put(Session.PID, pid); + session.put(Session.SERVER, server); + session.put(Session.TTY, term); + this.id = UUID.randomUUID().toString(); + session.put(Session.ID, id); + this.commandManager = commandManager; + this.closedFuture = Future.future(); + this.term = term; + this.jobController = jobController; + + if (term != null) { + term.setSession(session); + } + } + + public JobController jobController() { + return jobController; + } + + public Set jobs() { + return jobController.jobs(); + } + + @Override + public synchronized Job createJob(List args) { + Job job = jobController.createJob(commandManager, args, this); + return job; + } + + @Override + public Job createJob(String line) { + return createJob(CliTokens.tokenize(line)); + } + + @Override + public Session session() { + return session; + } + + public Term term() { + return term; + } + + public FutureHandler closedFutureHandler() { + return new FutureHandler(closedFuture); + } + + public long lastAccessedTime() { + return term.lastAccessedTime(); + } + + public void setWelcome(String welcome) { + this.welcome = welcome; + } + + public ShellImpl init() { + term.interruptHandler(new InterruptHandler(this)); + term.suspendHandler(new SuspendHandler(this)); + term.closeHandler(new CloseHandler(this)); + + if (welcome != null && welcome.length() > 0) { + term.write(welcome); + term.write("pid: " + session.get(Session.PID) + "\n"); + term.write("timestamp: " + System.currentTimeMillis() + "\n\n"); + } + return this; + } + + public String statusLine(Job job, ExecStatus status) { + StringBuilder sb = new StringBuilder("[").append(job.id()).append("]"); + if (this.session().equals(job.getSession())) { + sb.append("*"); + } + sb.append("\n"); + sb.append(" ").append(Character.toUpperCase(status.name().charAt(0))) + .append(status.name().substring(1).toLowerCase()); + sb.append(" ").append(job.line()).append("\n"); + sb.append(" execution count : ").append(job.process().times()).append("\n"); + sb.append(" start time : ").append(job.process().startTime()).append("\n"); + String cacheLocation = job.process().cacheLocation(); + if (cacheLocation != null) { + sb.append(" cache location : ").append(cacheLocation).append("\n"); + } + Date timeoutDate = job.timeoutDate(); + if (timeoutDate != null) { + sb.append(" timeout date : ").append(timeoutDate).append("\n"); + } + sb.append(" session : ").append(job.getSession().getSessionId()).append( + session.equals(job.getSession()) ? " (current)" : "").append("\n"); + return sb.toString(); + } + + public void readline() { + term.readline(Constants.DEFAULT_PROMPT, new ShellLineHandler(this), + new CommandManagerCompletionHandler(commandManager)); + } + + public void close(String reason) { + if (term != null) { + try { + term.write("session (" + session.getSessionId() + ") is closed because " + reason + "\n"); + } catch (Throwable t) { + // sometimes an NPE will be thrown during shutdown via web-socket, + // this ensures the shutdown process is finished properly + // see more: middleware-container/arthas/issues/206 + logger.error("ARTHAS-206", "Error writing data:", t); + } + term.close(); + } else { + jobController.close(closedFutureHandler()); + } + } + + public void setForegroundJob(Job job) { + currentForegroundJob = job; + } + + public Job getForegroundJob() { + return currentForegroundJob; + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/impl/ShellServerImpl.java b/core/src/main/java/com/taobao/arthas/core/shell/impl/ShellServerImpl.java new file mode 100644 index 00000000..840cc4ec --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/impl/ShellServerImpl.java @@ -0,0 +1,238 @@ +package com.taobao.arthas.core.shell.impl; + +import com.taobao.arthas.core.server.ArthasBootstrap; +import com.taobao.arthas.core.shell.Shell; +import com.taobao.arthas.core.shell.ShellServer; +import com.taobao.arthas.core.shell.ShellServerOptions; +import com.taobao.arthas.core.shell.command.CommandResolver; +import com.taobao.arthas.core.shell.future.Future; +import com.taobao.arthas.core.shell.handlers.Handler; +import com.taobao.arthas.core.shell.handlers.server.SessionClosedHandler; +import com.taobao.arthas.core.shell.handlers.server.SessionsClosedHandler; +import com.taobao.arthas.core.shell.handlers.server.TermServerListenHandler; +import com.taobao.arthas.core.shell.handlers.server.TermServerTermHandler; +import com.taobao.arthas.core.shell.system.Job; +import com.taobao.arthas.core.shell.system.impl.GlobalJobControllerImpl; +import com.taobao.arthas.core.shell.system.impl.InternalCommandManager; +import com.taobao.arthas.core.shell.system.impl.JobControllerImpl; +import com.taobao.arthas.core.shell.term.Term; +import com.taobao.arthas.core.shell.term.TermServer; +import com.taobao.arthas.core.util.LogUtil; +import com.taobao.middleware.logger.Logger; + +import java.lang.instrument.Instrumentation; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author Julien Viet + */ +public class ShellServerImpl extends ShellServer { + + private static final Logger logger = LogUtil.getArthasLogger(); + + private final CopyOnWriteArrayList resolvers; + private final InternalCommandManager commandManager; + private final List termServers; + private final long timeoutMillis; + private final long reaperInterval; + private String welcomeMessage; + private ArthasBootstrap bootstrap; + private Instrumentation instrumentation; + private int pid; + private boolean closed = true; + private final Map sessions; + private final Future sessionsClosed = Future.future(); + private ScheduledExecutorService scheduledExecutorService; + private JobControllerImpl jobController = new GlobalJobControllerImpl(); + + public ShellServerImpl(ShellServerOptions options) { + this(options, null); + } + + public ShellServerImpl(ShellServerOptions options, ArthasBootstrap bootstrap) { + this.welcomeMessage = options.getWelcomeMessage(); + this.termServers = new ArrayList(); + this.timeoutMillis = options.getSessionTimeout(); + this.sessions = new ConcurrentHashMap(); + this.reaperInterval = options.getReaperInterval(); + this.resolvers = new CopyOnWriteArrayList(); + this.commandManager = new InternalCommandManager(resolvers); + this.instrumentation = options.getInstrumentation(); + this.bootstrap = bootstrap; + this.pid = options.getPid(); + + // Register builtin commands so they are listed in help + resolvers.add(new BuiltinCommandResolver()); + } + + @Override + public synchronized ShellServer registerCommandResolver(CommandResolver resolver) { + resolvers.add(0, resolver); + return this; + } + + @Override + public synchronized ShellServer registerTermServer(TermServer termServer) { + termServers.add(termServer); + return this; + } + + public void handleTerm(Term term) { + synchronized (this) { + // That might happen with multiple ser + if (closed) { + term.close(); + return; + } + } + + ShellImpl session = createShell(term); + session.setWelcome(welcomeMessage); + session.closedFuture.setHandler(new SessionClosedHandler(this, session)); + session.init(); + sessions.put(session.id, session); // Put after init so the close handler on the connection is set + session.readline(); // Now readline + } + + @Override + public ShellServer listen(final Handler> listenHandler) { + final List toStart; + synchronized (this) { + if (!closed) { + throw new IllegalStateException("Server listening"); + } + toStart = termServers; + } + final AtomicInteger count = new AtomicInteger(toStart.size()); + if (count.get() == 0) { + setClosed(false); + listenHandler.handle(Future.succeededFuture()); + return this; + } + Handler> handler = new TermServerListenHandler(this, listenHandler, toStart); + for (TermServer termServer : toStart) { + termServer.termHandler(new TermServerTermHandler(this)); + termServer.listen(handler); + } + return this; + } + + private void evictSessions() { + long now = System.currentTimeMillis(); + Set toClose = new HashSet(); + for (ShellImpl session : sessions.values()) { + // do not close if there is still job running, + // e.g. trace command might wait for a long time before condition is met + if (now - session.lastAccessedTime() > timeoutMillis && session.jobs().size() == 0) { + toClose.add(session); + } + logger.debug(session.id + ":" + session.lastAccessedTime()); + } + for (ShellImpl session : toClose) { + long timeOutInMinutes = timeoutMillis / 1000 / 60; + String reason = "session is inactive for " + timeOutInMinutes + " min(s)."; + session.close(reason); + } + } + + public synchronized void setTimer() { + if (!closed && reaperInterval > 0) { + scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); + // TODO rename the thread, currently it is `pool-3-thread-1`, which is ambiguous + scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + + @Override + public void run() { + evictSessions(); + } + }, 0, reaperInterval, TimeUnit.MILLISECONDS); + } + } + + public synchronized void setClosed(boolean closed) { + this.closed = closed; + } + + public void removeSession(ShellImpl shell) { + boolean completeSessionClosed; + + Job job = shell.getForegroundJob(); + if (job != null) { + // close shell's foreground job + job.terminate(); + logger.info(null, "Session {} closed, so terminate foreground job, id: {}, line: {}", + shell.session().getSessionId(), job.id(), job.line()); + } + + synchronized (ShellServerImpl.this) { + sessions.remove(shell.id); + completeSessionClosed = sessions.isEmpty() && closed; + } + if (completeSessionClosed) { + sessionsClosed.complete(); + } + } + + @Override + public synchronized Shell createShell() { + return createShell(null); + } + + @Override + public synchronized ShellImpl createShell(Term term) { + if (closed) { + throw new IllegalStateException("Closed"); + } + return new ShellImpl(this, term, commandManager, instrumentation, pid, jobController); + } + + @Override + public void close(final Handler> completionHandler) { + List toStop; + List toClose; + synchronized (this) { + if (closed) { + toStop = Collections.emptyList(); + toClose = Collections.emptyList(); + } else { + setClosed(true); + if (scheduledExecutorService != null) { + scheduledExecutorService.shutdownNow(); + } + toStop = termServers; + toClose = new ArrayList(sessions.values()); + if (toClose.isEmpty()) { + sessionsClosed.complete(); + } + } + } + if (toStop.isEmpty() && toClose.isEmpty()) { + completionHandler.handle(Future.succeededFuture()); + } else { + final AtomicInteger count = new AtomicInteger(1 + toClose.size()); + Handler> handler = new SessionsClosedHandler(count, completionHandler); + + for (ShellImpl shell : toClose) { + shell.close("server is going to shutdown."); + } + + for (TermServer termServer : toStop) { + termServer.close(handler); + } + jobController.close(); + sessionsClosed.setHandler(handler); + bootstrap.destroy(); + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/session/Session.java b/core/src/main/java/com/taobao/arthas/core/shell/session/Session.java new file mode 100644 index 00000000..375799f3 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/session/Session.java @@ -0,0 +1,110 @@ +package com.taobao.arthas.core.shell.session; + +import com.taobao.arthas.core.shell.ShellServer; +import com.taobao.arthas.core.shell.command.CommandResolver; + +import java.lang.instrument.Instrumentation; +import java.util.List; + +/** + * A shell session. + * + * @author Julien Viet + */ +public interface Session { + String COMMAND_MANAGER = "arthas-command-manager"; + String PID = "pid"; + String INSTRUMENTATION = "instrumentation"; + String ID = "id"; + String SERVER = "server"; + /** + * The tty this session related to. + */ + String TTY = "tty"; + + /** + * Put some data in a session + * + * @param key the key for the data + * @param obj the data + * @return a reference to this, so the API can be used fluently + */ + Session put(String key, Object obj); + + /** + * Get some data from the session + * + * @param key the key of the data + * @return the data + */ + T get(String key); + + /** + * Remove some data from the session + * + * @param key the key of the data + * @return the data that was there or null if none there + */ + T remove(String key); + + /** + * Check if the session has been already locked + * + * @return locked or not + */ + boolean isLocked(); + + /** + * Unlock the session + * + */ + void unLock(); + + /** + * Try to fetch the current session's lock + * + * @return success or not + */ + boolean tryLock(); + + /** + * Check current lock's sequence id + * + * @return lock's sequence id + */ + int getLock(); + + /** + * Get session id + * @return session id + */ + String getSessionId(); + + /** + * Get shell server + * + * @return shell server + */ + ShellServer getServer(); + + /** + * Get Java PID + * + * @return java pid + */ + int getPid(); + + /** + * Get all registered command resolvers + * + * @return command resolvers + */ + List getCommandResolvers(); + + /** + * Get java instrumentation + * + * @return instrumentation instance + */ + Instrumentation getInstrumentation(); +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/session/impl/SessionImpl.java b/core/src/main/java/com/taobao/arthas/core/shell/session/impl/SessionImpl.java new file mode 100644 index 00000000..21756840 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/session/impl/SessionImpl.java @@ -0,0 +1,92 @@ +package com.taobao.arthas.core.shell.session.impl; + +import com.taobao.arthas.core.shell.ShellServer; +import com.taobao.arthas.core.shell.command.CommandResolver; +import com.taobao.arthas.core.shell.session.Session; +import com.taobao.arthas.core.shell.system.impl.InternalCommandManager; + +import java.lang.instrument.Instrumentation; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author Julien Viet + */ +public class SessionImpl implements Session { + private final static AtomicInteger lockSequence = new AtomicInteger(); + private final static int LOCK_TX_EMPTY = -1; + private final AtomicInteger lock = new AtomicInteger(LOCK_TX_EMPTY); + + private Map data = new HashMap(); + + @Override + public Session put(String key, Object obj) { + if (obj == null) { + data.remove(key); + } else { + data.put(key, obj); + } + return this; + } + + @Override + public T get(String key) { + return (T) data.get(key); + } + + @Override + public T remove(String key) { + return (T) data.remove(key); + } + + @Override + public boolean tryLock() { + return lock.compareAndSet(LOCK_TX_EMPTY, lockSequence.getAndIncrement()); + } + + @Override + public void unLock() { + int currentLockTx = lock.get(); + if (!lock.compareAndSet(currentLockTx, LOCK_TX_EMPTY)) { + throw new IllegalStateException(); + } + } + + @Override + public boolean isLocked() { + return lock.get() != LOCK_TX_EMPTY; + } + + @Override + public int getLock() { + return lock.get(); + } + + @Override + public String getSessionId() { + return (String) data.get(ID); + } + + @Override + public ShellServer getServer() { + return (ShellServer) data.get(SERVER); + } + + @Override + public int getPid() { + return (Integer) data.get(PID); + } + + @Override + public List getCommandResolvers() { + InternalCommandManager commandManager = (InternalCommandManager) data.get(COMMAND_MANAGER); + return commandManager.getResolvers(); + } + + @Override + public Instrumentation getInstrumentation() { + return (Instrumentation) data.get(INSTRUMENTATION); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/system/ExecStatus.java b/core/src/main/java/com/taobao/arthas/core/shell/system/ExecStatus.java new file mode 100644 index 00000000..f1199744 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/system/ExecStatus.java @@ -0,0 +1,31 @@ +package com.taobao.arthas.core.shell.system; + +/** + * The status of an execution. + * + * @author Julien Viet + */ +public enum ExecStatus { + + /** + * The job is ready, it can be running or terminated. + */ + READY, + + /** + * The job is running, it can be stopped or terminated. + */ + RUNNING, + + /** + * The job is stopped, it can be running or terminated. + */ + STOPPED, + + /** + * The job is terminated. + */ + TERMINATED + + +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/system/Job.java b/core/src/main/java/com/taobao/arthas/core/shell/system/Job.java new file mode 100644 index 00000000..1a9903da --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/system/Job.java @@ -0,0 +1,115 @@ +package com.taobao.arthas.core.shell.system; + +import java.util.Date; + +import com.taobao.arthas.core.shell.future.Future; +import com.taobao.arthas.core.shell.handlers.Handler; +import com.taobao.arthas.core.shell.session.Session; +import com.taobao.arthas.core.shell.term.Tty; + +/** + * A job executed in a {@link JobController}, grouping one or several process.

+ * + * The job life cycle can be controlled with the {@link #run}, {@link #resume} and {@link #suspend} and {@link #interrupt} + * methods. + * + * @author Julien Viet + */ +public interface Job { + + /** + * @return the job id + */ + int id(); + + /** + * @return the job exec status + */ + ExecStatus status(); + + /** + * @return the execution line of the job, i.e the shell command line that launched this job + */ + String line(); + + + /** + * Run the job, before running the job a {@link Tty} must be set. + * + * @return this object + */ + Job run(); + + /** + * Run the job, before running the job a {@link Tty} must be set. + * + * @return this object + */ + Job run(boolean foreground); + + /** + * Attempt to interrupt the job. + * + * @return true if the job is actually interrupted + */ + boolean interrupt(); + + /** + * Resume the job to foreground. + */ + Job resume(); + + /** + * Send the job to background. + * + * @return this object + */ + Job toBackground(); + + /** + * Send the job to foreground. + * + * @return this object + */ + Job toForeground(); + + /** + * Resume the job. + * + * @param foreground true when the job is resumed in foreground + */ + Job resume(boolean foreground); + + /** + * Resume the job. + * + * @return this object + */ + Job suspend(); + + /** + * Terminate the job. + */ + void terminate(); + + /** + * @return the first process in the job + */ + Process process(); + + /** + * @return the date with job timeout + */ + Date timeoutDate(); + + /** + * Set the date with job timeout + * @param date the date with job timeout + */ + void setTimeoutDate(Date date); + + /** + * @return the session this job belongs to + */ + Session getSession(); +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/system/JobController.java b/core/src/main/java/com/taobao/arthas/core/shell/system/JobController.java new file mode 100644 index 00000000..a0045486 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/system/JobController.java @@ -0,0 +1,51 @@ +package com.taobao.arthas.core.shell.system; + +import com.taobao.arthas.core.shell.cli.CliToken; +import com.taobao.arthas.core.shell.handlers.Handler; +import com.taobao.arthas.core.shell.impl.ShellImpl; +import com.taobao.arthas.core.shell.system.impl.InternalCommandManager; + +import java.util.List; +import java.util.Set; + +/** + * The job controller.

+ * + * @author Julien Viet + */ +public interface JobController { + + /** + * @return the active jobs + */ + Set jobs(); + + /** + * Returns an active job in this session by its {@literal id}. + * + * @param id the job id + * @return the job of {@literal null} when not found + */ + Job getJob(int id); + + /** + * Create a job wrapping a process. + * + * @param commandManager command manager + * @param tokens the command tokens + * @param shell the current shell + * @return the created job + */ + Job createJob(InternalCommandManager commandManager, List tokens, ShellImpl shell); + + /** + * Close the controller and terminate all the underlying jobs, a closed controller does not accept anymore jobs. + */ + void close(Handler completionHandler); + + /** + * Close the shell session and terminate all the underlying jobs. + */ + void close(); + +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/system/Process.java b/core/src/main/java/com/taobao/arthas/core/shell/system/Process.java new file mode 100644 index 00000000..8d42d277 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/system/Process.java @@ -0,0 +1,178 @@ +package com.taobao.arthas.core.shell.system; + +import java.io.File; +import java.util.Date; + +import com.taobao.arthas.core.shell.handlers.Handler; +import com.taobao.arthas.core.shell.session.Session; +import com.taobao.arthas.core.shell.term.Tty; + +/** + * A process managed by the shell. + * + * @author Julien Viet + */ +public interface Process { + /** + * @return the current process status + */ + ExecStatus status(); + + /** + * @return the process exit code when the status is {@link ExecStatus#TERMINATED} otherwise {@code null} + */ + Integer exitCode(); + + /** + * Set the process tty. + * + * @param tty the process tty + * @return this object + */ + Process setTty(Tty tty); + + /** + * @return the process tty + */ + Tty getTty(); + + /** + * Set the process session + * + * @param session the process session + * @return this object + */ + Process setSession(Session session); + + /** + * @return the process session + */ + Session getSession(); + + /** + * Set an handler for being notified when the process terminates. + * + * @param handler the handler called when the process terminates. + * @return this object + */ + Process terminatedHandler(Handler handler); + + /** + * Run the process. + */ + void run(); + + /** + * Run the process. + */ + void run(boolean foreground); + + /** + * Attempt to interrupt the process. + * + * @return true if the process caught the signal + */ + boolean interrupt(); + + /** + * Attempt to interrupt the process. + * + * @param completionHandler handler called after interrupt callback + * @return true if the process caught the signal + */ + boolean interrupt(Handler completionHandler); + + /** + * Suspend the process. + */ + void resume(); + + /** + * Suspend the process. + */ + void resume(boolean foreground); + + /** + * Suspend the process. + * + * @param completionHandler handler called after resume callback + */ + void resume(Handler completionHandler); + + /** + * Suspend the process. + * + * @param completionHandler handler called after resume callback + */ + void resume(boolean foreground, Handler completionHandler); + + /** + * Resume the process. + */ + void suspend(); + + /** + * Resume the process. + * + * @param completionHandler handler called after suspend callback + */ + void suspend(Handler completionHandler); + + /** + * Terminate the process. + */ + void terminate(); + + /** + * Terminate the process. + * + * @param completionHandler handler called after end callback + */ + void terminate(Handler completionHandler); + + /** + * Set the process in background. + */ + void toBackground(); + + /** + * Set the process in background. + * + * @param completionHandler handler called after background callback + */ + void toBackground(Handler completionHandler); + + /** + * Set the process in foreground. + */ + void toForeground(); + + /** + * Set the process in foreground. + * + * @param completionHandler handler called after foreground callback + */ + void toForeground(Handler completionHandler); + + /** + * Execution times + */ + int times(); + + /** + * Build time + */ + Date startTime(); + + /** + * Get cache file location + */ + String cacheLocation(); + + /** + * Set job id + * + * @param jobId job id + */ + void setJobId(int jobId); +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/system/impl/CommandCompletion.java b/core/src/main/java/com/taobao/arthas/core/shell/system/impl/CommandCompletion.java new file mode 100644 index 00000000..f0c6ab63 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/system/impl/CommandCompletion.java @@ -0,0 +1,47 @@ +package com.taobao.arthas.core.shell.system.impl; + +import com.taobao.arthas.core.shell.cli.CliToken; +import com.taobao.arthas.core.shell.cli.Completion; +import com.taobao.arthas.core.shell.session.Session; + +import java.util.List; + +/** + * @author beiwei30 on 23/11/2016. + */ +class CommandCompletion implements Completion { + private final Completion completion; + private final String line; + private final List newTokens; + + public CommandCompletion(Completion completion, String line, List newTokens) { + this.completion = completion; + this.line = line; + this.newTokens = newTokens; + } + + @Override + public Session session() { + return completion.session(); + } + + @Override + public String rawLine() { + return line; + } + + @Override + public List lineTokens() { + return newTokens; + } + + @Override + public void complete(List candidates) { + completion.complete(candidates); + } + + @Override + public void complete(String value, boolean terminal) { + completion.complete(value, terminal); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/system/impl/GlobalJobControllerImpl.java b/core/src/main/java/com/taobao/arthas/core/shell/system/impl/GlobalJobControllerImpl.java new file mode 100644 index 00000000..e67f8470 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/system/impl/GlobalJobControllerImpl.java @@ -0,0 +1,109 @@ +package com.taobao.arthas.core.shell.system.impl; + +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.TimeUnit; + +import com.taobao.arthas.core.GlobalOptions; +import com.taobao.arthas.core.shell.cli.CliToken; +import com.taobao.arthas.core.shell.handlers.Handler; +import com.taobao.arthas.core.shell.impl.ShellImpl; +import com.taobao.arthas.core.shell.system.Job; +import com.taobao.arthas.core.util.LogUtil; +import com.taobao.middleware.logger.Logger; + +/** + * 全局的Job Controller,不应该存在启停的概念,不需要在连接的断开时关闭, + * + * @author gehui 2017年7月31日 上午11:55:41 + */ +public class GlobalJobControllerImpl extends JobControllerImpl { + + private Timer timer = new Timer("job-timeout", true); + private Map jobTimeoutTaskMap = new HashMap(); + private static final Logger logger = LogUtil.getArthasLogger(); + + @Override + public void close(final Handler completionHandler) { + if (completionHandler != null) { + completionHandler.handle(null); + } + } + + @Override + public void close() { + timer.cancel(); + jobTimeoutTaskMap.clear(); + for (Job job : jobs()) { + job.terminate(); + } + } + + @Override + public boolean removeJob(int id) { + TimerTask jobTimeoutTask = jobTimeoutTaskMap.remove(id); + if (jobTimeoutTask != null) { + jobTimeoutTask.cancel(); + } + return super.removeJob(id); + }; + + @Override + public Job createJob(InternalCommandManager commandManager, List tokens, ShellImpl shell) { + final Job job = super.createJob(commandManager, tokens, shell); + + /* + * 达到超时时间将会停止job + */ + TimerTask jobTimeoutTask = new TimerTask() { + @Override + public void run() { + job.terminate(); + } + }; + Date timeoutDate = new Date(System.currentTimeMillis() + (getJobTimeoutInSecond() * 1000)); + timer.schedule(jobTimeoutTask, timeoutDate); + jobTimeoutTaskMap.put(job.id(), jobTimeoutTask); + job.setTimeoutDate(timeoutDate); + + return job; + } + + private long getJobTimeoutInSecond() { + long result = -1; + String jobTimeoutConfig = GlobalOptions.jobTimeout.trim(); + try { + char unit = jobTimeoutConfig.charAt(jobTimeoutConfig.length() - 1); + String duration = jobTimeoutConfig.substring(0, jobTimeoutConfig.length() - 1); + switch (unit) { + case 'h': + result = TimeUnit.HOURS.toSeconds(Long.parseLong(duration)); + break; + case 'd': + result = TimeUnit.DAYS.toSeconds(Long.parseLong(duration)); + break; + case 'm': + result = TimeUnit.MINUTES.toSeconds(Long.parseLong(duration)); + break; + case 's': + result = Long.parseLong(duration); + break; + default: + result = Long.parseLong(jobTimeoutConfig); + break; + } + } catch (Exception e) { + } + + if (result < 0) { + // 如果设置的属性有错误,那么使用默认的1天 + result = TimeUnit.DAYS.toSeconds(1); + logger.warn("Configuration with job timeout " + jobTimeoutConfig + " is error, use 1d in default."); + } + return result; + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/system/impl/InternalCommandManager.java b/core/src/main/java/com/taobao/arthas/core/shell/system/impl/InternalCommandManager.java new file mode 100644 index 00000000..967314c1 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/system/impl/InternalCommandManager.java @@ -0,0 +1,147 @@ +package com.taobao.arthas.core.shell.system.impl; + +import com.taobao.arthas.core.command.BuiltinCommandPack; +import com.taobao.arthas.core.shell.cli.CliToken; +import com.taobao.arthas.core.shell.cli.Completion; +import com.taobao.arthas.core.shell.cli.CompletionUtils; +import com.taobao.arthas.core.shell.command.Command; +import com.taobao.arthas.core.shell.command.CommandResolver; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; + +/** + * @author Julien Viet + */ +public class InternalCommandManager { + + private final List resolvers; + + public InternalCommandManager(CommandResolver... resolvers) { + this.resolvers = Arrays.asList(resolvers); + } + + public InternalCommandManager(List resolvers) { + this.resolvers = resolvers; + } + + public List getResolvers() { + return resolvers; + } + + public Command getCommand(String commandName) { + Command command = null; + for (CommandResolver resolver : resolvers) { + // 内建命令在ShellLineHandler里提前处理了,所以这里不需要再查找内建命令 + if (resolver instanceof BuiltinCommandPack) { + command = getCommand(resolver, commandName); + if (command != null) { + break; + } + } + } + return command; + } + + /** + * Perform completion, the completion argument will be notified of the completion progress. + * + * @param completion the completion object + */ + public void complete(final Completion completion) { + List lineTokens = completion.lineTokens(); + int index = findLastPipe(lineTokens); + LinkedList tokens = new LinkedList(lineTokens.subList(index + 1, lineTokens.size())); + + // Remove any leading white space + while (tokens.size() > 0 && tokens.getFirst().isBlank()) { + tokens.removeFirst(); + } + + // > 1 means it's a text token followed by something else + if (tokens.size() > 1) { + completeSingleCommand(completion, tokens); + } else { + completeCommands(completion, tokens); + } + } + + private void completeCommands(Completion completion, LinkedList tokens) { + String prefix = tokens.size() > 0 ? tokens.getFirst().value() : ""; + List names = new LinkedList(); + for (CommandResolver resolver : resolvers) { + for (Command command : resolver.commands()) { + String name = command.name(); + boolean hidden = command.cli() != null && command.cli().isHidden(); + if (name.startsWith(prefix) && !names.contains(name) && !hidden) { + names.add(name); + } + } + } + if (names.size() == 1) { + completion.complete(names.get(0).substring(prefix.length()), true); + } else { + String commonPrefix = CompletionUtils.findLongestCommonPrefix(names); + if (commonPrefix.length() > prefix.length()) { + completion.complete(commonPrefix.substring(prefix.length()), false); + } else { + completion.complete(names); + } + } + } + + private void completeSingleCommand(Completion completion, LinkedList tokens) { + ListIterator it = tokens.listIterator(); + while (it.hasNext()) { + CliToken ct = it.next(); + it.remove(); + if (ct.isText()) { + final List newTokens = new ArrayList(); + while (it.hasNext()) { + newTokens.add(it.next()); + } + StringBuilder tmp = new StringBuilder(); + for (CliToken token : newTokens) { + tmp.append(token.raw()); + } + final String line = tmp.toString(); + for (CommandResolver resolver : resolvers) { + Command command = getCommand(resolver, ct.value()); + if (command != null) { + command.complete(new CommandCompletion(completion, line, newTokens)); + return; + } + } + completion.complete(Collections.emptyList()); + } + } + } + + private static Command getCommand(CommandResolver commandResolver, String name) { + List commands = commandResolver.commands(); + if (commands == null || commands.isEmpty()) { + return null; + } + + for (Command command : commands) { + if (name.equals(command.name())) { + return command; + } + } + return null; + } + + private static int findLastPipe(List lineTokens) { + int index = -1; + for (int i = 0; i < lineTokens.size(); i++) { + if ("|".equals(lineTokens.get(i).value())) { + index = i; + } + } + return index; + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/system/impl/JobControllerImpl.java b/core/src/main/java/com/taobao/arthas/core/shell/system/impl/JobControllerImpl.java new file mode 100644 index 00000000..5e6c066e --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/system/impl/JobControllerImpl.java @@ -0,0 +1,226 @@ +package com.taobao.arthas.core.shell.system.impl; + +import com.taobao.arthas.core.GlobalOptions; +import com.taobao.arthas.core.shell.cli.CliToken; +import com.taobao.arthas.core.shell.command.Command; +import com.taobao.arthas.core.shell.command.internal.RedirectHandler; +import com.taobao.arthas.core.shell.command.internal.StdoutHandler; +import com.taobao.arthas.core.shell.command.internal.TermHandler; +import com.taobao.arthas.core.shell.future.Future; +import com.taobao.arthas.core.shell.handlers.Handler; +import com.taobao.arthas.core.shell.impl.ShellImpl; +import com.taobao.arthas.core.shell.system.Job; +import com.taobao.arthas.core.shell.system.JobController; +import com.taobao.arthas.core.shell.system.Process; +import com.taobao.arthas.core.shell.system.impl.ProcessImpl.ProcessOutput; +import com.taobao.arthas.core.shell.term.Term; +import com.taobao.arthas.core.util.Constants; +import com.taobao.arthas.core.util.TokenUtils; + +import io.termd.core.function.Function; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.ListIterator; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author Julien Viet + */ +public class JobControllerImpl implements JobController { + + private final SortedMap jobs = new TreeMap(); + private final AtomicInteger idGenerator = new AtomicInteger(0); + private boolean closed = false; + + public JobControllerImpl() { + } + + public synchronized Set jobs() { + return new HashSet(jobs.values()); + } + + public synchronized Job getJob(int id) { + return jobs.get(id); + } + + synchronized boolean removeJob(int id) { + return jobs.remove(id) != null; + } + + @Override + public Job createJob(InternalCommandManager commandManager, List tokens, ShellImpl shell) { + int jobId = idGenerator.incrementAndGet(); + StringBuilder line = new StringBuilder(); + for (CliToken arg : tokens) { + line.append(arg.raw()); + } + boolean runInBackground = runInBackground(tokens); + Process process = createProcess(tokens, commandManager, jobId, shell.term()); + process.setJobId(jobId); + JobImpl job = new JobImpl(jobId, this, process, line.toString(), runInBackground, shell); + jobs.put(jobId, job); + return job; + } + + private int getRedirectJobCount() { + int count = 0; + for (Job job : jobs.values()) { + if (job.process() != null && job.process().cacheLocation() != null) { + count++; + } + } + return count; + } + + @Override + public void close(final Handler completionHandler) { + List jobs; + synchronized (this) { + if (closed) { + jobs = Collections.emptyList(); + } else { + jobs = new ArrayList(this.jobs.values()); + closed = true; + } + } + if (jobs.isEmpty()) { + if (completionHandler!= null) { + completionHandler.handle(null); + } + } else { + final AtomicInteger count = new AtomicInteger(jobs.size()); + for (JobImpl job : jobs) { + job.terminateFuture.setHandler(new Handler>() { + @Override + public void handle(Future v) { + if (count.decrementAndGet() == 0 && completionHandler != null) { + completionHandler.handle(null); + } + } + }); + job.terminate(); + } + } + } + + /** + * Try to create a process from the command line tokens. + * + * @param line the command line tokens + * @param commandManager command manager + * @param jobId job id + * @param term term + * @return the created process + */ + private Process createProcess(List line, InternalCommandManager commandManager, int jobId, Term term) { + try { + ListIterator tokens = line.listIterator(); + while (tokens.hasNext()) { + CliToken token = tokens.next(); + if (token.isText()) { + Command command = commandManager.getCommand(token.value()); + if (command != null) { + return createCommandProcess(command, tokens, jobId, term); + } else { + throw new IllegalArgumentException(token.value() + ": command not found"); + } + } + } + throw new IllegalArgumentException(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private boolean runInBackground(List tokens) { + boolean runInBackground = false; + CliToken last = TokenUtils.findLastTextToken(tokens); + if (last != null && "&".equals(last.value())) { + runInBackground = true; + tokens.remove(last); + } + return runInBackground; + } + + private Process createCommandProcess(Command command, ListIterator tokens, int jobId, Term term) { + List remaining = new ArrayList(); + List pipelineTokens = new ArrayList(); + boolean isPipeline = false; + RedirectHandler redirectHandler = null; + List> stdoutHandlerChain = new ArrayList>(); + String cacheLocation = null; + while (tokens.hasNext()) { + CliToken remainingToken = tokens.next(); + if (remainingToken.isText()) { + String tokenValue = remainingToken.value(); + if ("|".equals(tokenValue)) { + isPipeline = true; + // 将管道符|之后的部分注入为输出链上的handler + injectHandler(stdoutHandlerChain, pipelineTokens); + continue; + } else if (">>".equals(tokenValue) || ">".equals(tokenValue)) { + String name = getRedirectFileName(tokens); + if (name == null) { + // 如果没有指定重定向文件名,那么重定向到以jobid命名的缓存中 + name = Constants.PID + File.separator + jobId; + cacheLocation = Constants.CACHE_ROOT + File.separator + name; + + if (getRedirectJobCount() == 8) { + throw new IllegalStateException("The amount of async command that saving result to file can't > 8"); + } + } + redirectHandler = new RedirectHandler(name); + break; + } + } + if (isPipeline) { + pipelineTokens.add(remainingToken); + } else { + remaining.add(remainingToken); + } + } + injectHandler(stdoutHandlerChain, pipelineTokens); + if (redirectHandler != null) { + stdoutHandlerChain.add(redirectHandler); + } else { + stdoutHandlerChain.add(new TermHandler(term)); + if (GlobalOptions.isSaveResult) { + stdoutHandlerChain.add(new RedirectHandler()); + } + } + ProcessOutput ProcessOutput = new ProcessOutput(stdoutHandlerChain, cacheLocation, term); + return new ProcessImpl(command, remaining, command.processHandler(), ProcessOutput); + } + + private String getRedirectFileName(ListIterator tokens) { + while (tokens.hasNext()) { + CliToken token = tokens.next(); + if (token.isText()) { + return token.value(); + } + } + return null; + } + + private void injectHandler(List> stdoutHandlerChain, List pipelineTokens) { + if (!pipelineTokens.isEmpty()) { + StdoutHandler handler = StdoutHandler.inject(pipelineTokens); + if (handler != null) { + stdoutHandlerChain.add(handler); + } + pipelineTokens.clear(); + } + } + + @Override + public void close() { + close(null); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/system/impl/JobImpl.java b/core/src/main/java/com/taobao/arthas/core/shell/system/impl/JobImpl.java new file mode 100644 index 00000000..1dd90213 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/system/impl/JobImpl.java @@ -0,0 +1,254 @@ +package com.taobao.arthas.core.shell.system.impl; + +import java.util.Date; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.taobao.arthas.core.shell.future.Future; +import com.taobao.arthas.core.shell.handlers.Handler; +import com.taobao.arthas.core.shell.handlers.shell.ShellForegroundUpdateHandler; +import com.taobao.arthas.core.shell.impl.ShellImpl; +import com.taobao.arthas.core.shell.session.Session; +import com.taobao.arthas.core.shell.system.ExecStatus; +import com.taobao.arthas.core.shell.system.Job; +import com.taobao.arthas.core.shell.system.Process; + +/** + * @author Julien Viet + */ +public class JobImpl implements Job { + + final int id; + final JobControllerImpl controller; + final Process process; + final String line; + private volatile ExecStatus actualStatus; // Used internally for testing only + volatile long lastStopped; // When the job was last stopped + volatile ShellImpl shell; + volatile Handler statusUpdateHandler; + volatile Date timeoutDate; + final Future terminateFuture; + final AtomicBoolean runInBackground; + final Handler foregroundUpdatedHandler; + + JobImpl(int id, final JobControllerImpl controller, Process process, String line, boolean runInBackground, + ShellImpl shell) { + this.id = id; + this.controller = controller; + this.process = process; + this.line = line; + this.terminateFuture = Future.future(); + this.runInBackground = new AtomicBoolean(runInBackground); + this.shell = shell; + this.foregroundUpdatedHandler = new ShellForegroundUpdateHandler(shell); + process.terminatedHandler(new TerminatedHandler(controller)); + } + + public ExecStatus actualStatus() { + return actualStatus; + } + + @Override + public boolean interrupt() { + return process.interrupt(); + } + + @Override + public Job resume() { + return resume(true); + } + + @Override + public Date timeoutDate() { + return timeoutDate; + } + + @Override + public void setTimeoutDate(Date date) { + this.timeoutDate = date; + } + + @Override + public Session getSession() { + return shell.session(); + } + + @Override + public Job resume(boolean foreground) { + try { + process.resume(new ResumeHandler()); + } catch (IllegalStateException ignore) { + + } + + runInBackground.set(!foreground); + + if (foreground) { + if (foregroundUpdatedHandler != null) { + foregroundUpdatedHandler.handle(this); + } + } + if (statusUpdateHandler != null) { + statusUpdateHandler.handle(process.status()); + } + + if (foreground) { + shell.setForegroundJob(this); + } else { + shell.setForegroundJob(null); + } + return this; + } + + @Override + public Job suspend() { + try { + process.suspend(new SuspendHandler()); + } catch (IllegalStateException ignore) { + return this; + } + if (!runInBackground.get() && foregroundUpdatedHandler != null) { + foregroundUpdatedHandler.handle(null); + } + if (statusUpdateHandler != null) { + statusUpdateHandler.handle(process.status()); + } + + shell.setForegroundJob(null); + return this; + } + + @Override + public void terminate() { + try { + process.terminate(); + } catch (IllegalStateException ignore) { + // Process already terminated, likely by itself + } + controller.removeJob(this.id); + } + + @Override + public Process process() { + return process; + } + + public ExecStatus status() { + return process.status(); + } + + public String line() { + return line; + } + + @Override + public Job toBackground() { + if (!this.runInBackground.get()) { + // run in foreground mode + if (runInBackground.compareAndSet(false, true)) { + process.toBackground(); + if (statusUpdateHandler != null) { + statusUpdateHandler.handle(process.status()); + } + } + } + + shell.setForegroundJob(null); + return this; + } + + @Override + public Job toForeground() { + if (this.runInBackground.get()) { + if (runInBackground.compareAndSet(true, false)) { + if (foregroundUpdatedHandler != null) { + foregroundUpdatedHandler.handle(this); + } + process.toForeground(); + if (statusUpdateHandler != null) { + statusUpdateHandler.handle(process.status()); + } + + shell.setForegroundJob(this); + } + } + + return this; + } + + @Override + public int id() { + return id; + } + + @Override + public Job run() { + return run(!runInBackground.get()); + } + + @Override + public Job run(boolean foreground) { + if (foreground && foregroundUpdatedHandler != null) { + foregroundUpdatedHandler.handle(this); + } + + actualStatus = ExecStatus.RUNNING; + if (statusUpdateHandler != null) { + statusUpdateHandler.handle(ExecStatus.RUNNING); + } + process.setTty(shell.term()); + process.setSession(shell.session()); + process.run(foreground); + + if (!foreground && foregroundUpdatedHandler != null) { + foregroundUpdatedHandler.handle(null); + } + + if (foreground) { + shell.setForegroundJob(this); + } else { + shell.setForegroundJob(null); + } + return this; + } + + private class TerminatedHandler implements Handler { + + private final JobControllerImpl controller; + + public TerminatedHandler(JobControllerImpl controller) { + this.controller = controller; + } + + @Override + public void handle(Integer exitCode) { + if (!runInBackground.get() && actualStatus.equals(ExecStatus.RUNNING)) { + // 只有前台在运行的任务,才需要调用foregroundUpdateHandler + if (foregroundUpdatedHandler != null) { + foregroundUpdatedHandler.handle(null); + } + } + controller.removeJob(JobImpl.this.id); + if (statusUpdateHandler != null) { + statusUpdateHandler.handle(ExecStatus.TERMINATED); + } + terminateFuture.complete(); + + } + } + + private class ResumeHandler implements Handler { + + @Override + public void handle(Void event) { + actualStatus = ExecStatus.RUNNING; + } + } + + private class SuspendHandler implements Handler { + + @Override + public void handle(Void event) { + actualStatus = ExecStatus.STOPPED; + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/system/impl/ProcessImpl.java b/core/src/main/java/com/taobao/arthas/core/shell/system/impl/ProcessImpl.java new file mode 100644 index 00000000..6134e123 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/system/impl/ProcessImpl.java @@ -0,0 +1,624 @@ +package com.taobao.arthas.core.shell.system.impl; + +import com.taobao.arthas.core.advisor.AdviceListener; +import com.taobao.arthas.core.advisor.AdviceWeaver; +import com.taobao.arthas.core.server.ArthasBootstrap; +import com.taobao.arthas.core.shell.cli.CliToken; +import com.taobao.arthas.core.shell.command.Command; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.shell.command.internal.CloseFunction; +import com.taobao.arthas.core.shell.command.internal.StatisticsFunction; +import com.taobao.arthas.core.shell.handlers.Handler; +import com.taobao.arthas.core.shell.session.Session; +import com.taobao.arthas.core.shell.system.ExecStatus; +import com.taobao.arthas.core.shell.system.Process; +import com.taobao.arthas.core.shell.term.Tty; +import com.taobao.arthas.core.util.LogUtil; +import com.taobao.arthas.core.util.usage.StyledUsageFormatter; +import com.taobao.middleware.cli.CLIException; +import com.taobao.middleware.cli.CommandLine; +import com.taobao.middleware.cli.UsageMessageFormatter; +import com.taobao.middleware.logger.Logger; +import com.taobao.text.Color; + +import io.termd.core.function.Function; + +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author beiwei30 on 10/11/2016. + */ +public class ProcessImpl implements Process { + + private static final Logger logger = LogUtil.getArthasLogger(); + + private Command commandContext; + private Handler handler; + private List args; + private Tty tty; + private Session session; + private Handler interruptHandler; + private Handler suspendHandler; + private Handler resumeHandler; + private Handler endHandler; + private Handler backgroundHandler; + private Handler foregroundHandler; + private Handler terminatedHandler; + private boolean foreground; + private ExecStatus processStatus; + private boolean processForeground; + private Handler stdinHandler; + private Handler resizeHandler; + private Integer exitCode; + private CommandProcess process; + private Date startTime; + private ProcessOutput processOutput; + private int jobId; + + public ProcessImpl(Command commandContext, List args, Handler handler, + ProcessOutput processOutput) { + this.commandContext = commandContext; + this.handler = handler; + this.args = args; + this.processStatus = ExecStatus.READY; + this.processOutput = processOutput; + } + + @Override + public Integer exitCode() { + return exitCode; + } + + @Override + public ExecStatus status() { + return processStatus; + } + + @Override + public synchronized Process setTty(Tty tty) { + this.tty = tty; + return this; + } + + @Override + public synchronized Tty getTty() { + return tty; + } + + @Override + public void setJobId(int jobId) { + this.jobId = jobId; + } + + @Override + public synchronized Process setSession(Session session) { + this.session = session; + return this; + } + + @Override + public synchronized Session getSession() { + return session; + } + + @Override + public int times() { + return process.times().get(); + } + + public Date startTime() { + return startTime; + } + + @Override + public String cacheLocation() { + if (processOutput != null) { + return processOutput.cacheLocation; + } + return null; + } + + @Override + public Process terminatedHandler(Handler handler) { + terminatedHandler = handler; + return this; + } + + @Override + public boolean interrupt() { + return interrupt(null); + } + + @Override + public boolean interrupt(final Handler completionHandler) { + if (processStatus == ExecStatus.RUNNING || processStatus == ExecStatus.STOPPED) { + final Handler handler = interruptHandler; + try { + if (handler != null) { + handler.handle(null); + } + } finally { + if (completionHandler != null) { + completionHandler.handle(null); + } + } + return handler != null; + } else { + throw new IllegalStateException("Cannot interrupt process in " + processStatus + " state"); + } + } + + @Override + public void resume() { + resume(true); + } + + @Override + public void resume(boolean foreground) { + resume(foreground, null); + } + + @Override + public void resume(Handler completionHandler) { + resume(true, completionHandler); + } + + @Override + public synchronized void resume(boolean fg, Handler completionHandler) { + if (processStatus == ExecStatus.STOPPED) { + updateStatus(ExecStatus.RUNNING, null, fg, resumeHandler, terminatedHandler, completionHandler); + if (process != null) { + process.resume(); + } + } else { + throw new IllegalStateException("Cannot resume process in " + processStatus + " state"); + } + } + + @Override + public void suspend() { + suspend(null); + } + + @Override + public synchronized void suspend(Handler completionHandler) { + if (processStatus == ExecStatus.RUNNING) { + updateStatus(ExecStatus.STOPPED, null, false, suspendHandler, terminatedHandler, completionHandler); + if (process != null) { + process.suspend(); + } + } else { + throw new IllegalStateException("Cannot suspend process in " + processStatus + " state"); + } + } + + @Override + public void toBackground() { + toBackground(null); + } + + @Override + public void toBackground(Handler completionHandler) { + if (processStatus == ExecStatus.RUNNING) { + if (processForeground) { + updateStatus(ExecStatus.RUNNING, null, false, backgroundHandler, terminatedHandler, completionHandler); + } + } else { + throw new IllegalStateException("Cannot set to background a process in " + processStatus + " state"); + } + } + + @Override + public void toForeground() { + toForeground(null); + } + + @Override + public void toForeground(Handler completionHandler) { + if (processStatus == ExecStatus.RUNNING) { + if (!processForeground) { + updateStatus(ExecStatus.RUNNING, null, true, foregroundHandler, terminatedHandler, completionHandler); + } + } else { + throw new IllegalStateException("Cannot set to foreground a process in " + processStatus + " state"); + } + } + + @Override + public void terminate() { + terminate(null); + } + + @Override + public void terminate(Handler completionHandler) { + if (!terminate(-10, completionHandler)) { + throw new IllegalStateException("Cannot terminate terminated process"); + } + } + + private synchronized boolean terminate(int exitCode, Handler completionHandler) { + if (processStatus != ExecStatus.TERMINATED) { + if (process != null) { + processOutput.close(); + } + updateStatus(ExecStatus.TERMINATED, exitCode, false, endHandler, terminatedHandler, completionHandler); + if (process != null) { + process.unregister(); + } + return true; + } else { + return false; + } + } + + private void updateStatus(ExecStatus statusUpdate, Integer exitCodeUpdate, boolean foregroundUpdate, + Handler handler, Handler terminatedHandler, + Handler completionHandler) { + processStatus = statusUpdate; + exitCode = exitCodeUpdate; + if (!foregroundUpdate) { + if (processForeground) { + processForeground = false; + if (stdinHandler != null) { + tty.stdinHandler(null); + } + if (resizeHandler != null) { + tty.resizehandler(null); + } + } + } else { + if (!processForeground) { + processForeground = true; + if (stdinHandler != null) { + tty.stdinHandler(stdinHandler); + } + if (resizeHandler != null) { + tty.resizehandler(resizeHandler); + } + } + } + + foreground = foregroundUpdate; + try { + if (handler != null) { + handler.handle(null); + } + } finally { + if (completionHandler != null) { + completionHandler.handle(null); + } + if (terminatedHandler != null && statusUpdate == ExecStatus.TERMINATED) { + terminatedHandler.handle(exitCodeUpdate); + } + } + } + + @Override + public void run() { + run(true); + } + + @Override + public synchronized void run(boolean fg) { + if (processStatus != ExecStatus.READY) { + throw new IllegalStateException("Cannot run proces in " + processStatus + " state"); + } + + processStatus = ExecStatus.RUNNING; + processForeground = fg; + foreground = fg; + startTime = new Date(); + + // Make a local copy + final Tty tty = this.tty; + if (tty == null) { + throw new IllegalStateException("Cannot execute process without a TTY set"); + } + + final List args2 = new LinkedList(); + for (CliToken arg : args) { + if (arg.isText()) { + args2.add(arg.value()); + } + } + + CommandLine cl = null; + try { + if (commandContext.cli() != null) { + if (commandContext.cli().parse(args2, false).isAskingForHelp()) { + UsageMessageFormatter formatter = new StyledUsageFormatter(Color.green); + formatter.setWidth(tty.width()); + StringBuilder usage = new StringBuilder(); + commandContext.cli().usage(usage, formatter); + usage.append('\n'); + tty.write(usage.toString()); + terminate(); + return; + } + + cl = commandContext.cli().parse(args2); + } + } catch (CLIException e) { + tty.write(e.getMessage() + "\n"); + terminate(); + return; + } + + process = new CommandProcessImpl(args2, tty, cl); + if (cacheLocation() != null) { + process.echoTips("job id : " + this.jobId + "\n"); + process.echoTips("cache location : " + cacheLocation() + "\n"); + } + Runnable task = new CommandProcessTask(process); + ArthasBootstrap.getInstance().execute(task); + } + + private class CommandProcessTask implements Runnable { + + private CommandProcess process; + + public CommandProcessTask(CommandProcess process) { + this.process = process; + } + + @Override + public void run() { + try { + handler.handle(process); + } catch (Throwable t) { + logger.error(null, "Error during processing the command:", t); + process.write("Error during processing the command: " + t.getMessage() + "\n"); + terminate(1, null); + } + } + } + + private class CommandProcessImpl implements CommandProcess { + + private final List args2; + private final Tty tty; + private final CommandLine commandLine; + private int enhanceLock = -1; + private AtomicInteger times = new AtomicInteger(); + private AdviceListener suspendedListener = null; + + public CommandProcessImpl(List args2, Tty tty, CommandLine commandLine) { + this.args2 = args2; + this.tty = tty; + this.commandLine = commandLine; + } + + @Override + public List argsTokens() { + return args; + } + + @Override + public List args() { + return args2; + } + + @Override + public String type() { + return tty.type(); + } + + @Override + public boolean isForeground() { + return foreground; + } + + @Override + public int width() { + return tty.width(); + } + + @Override + public int height() { + return tty.height(); + } + + @Override + public CommandLine commandLine() { + return commandLine; + } + + @Override + public Session session() { + return session; + } + + @Override + public AtomicInteger times() { + return times; + } + + @Override + public CommandProcess stdinHandler(Handler handler) { + stdinHandler = handler; + if (processForeground && stdinHandler != null) { + tty.stdinHandler(stdinHandler); + } + return this; + } + + @Override + public CommandProcess write(String data) { + synchronized (ProcessImpl.this) { + if (processStatus != ExecStatus.RUNNING) { + throw new IllegalStateException( + "Cannot write to standard output when " + status().name().toLowerCase()); + } + } + processOutput.write(data); + return this; + } + + @Override + public void echoTips(String tips) { + processOutput.term.write(tips); + } + + @Override + public String cacheLocation() { + return ProcessImpl.this.cacheLocation(); + } + + @Override + public CommandProcess resizehandler(Handler handler) { + resizeHandler = handler; + tty.resizehandler(resizeHandler); + return this; + } + + @Override + public CommandProcess interruptHandler(Handler handler) { + synchronized (ProcessImpl.this) { + interruptHandler = handler; + } + return this; + } + + @Override + public CommandProcess suspendHandler(Handler handler) { + synchronized (ProcessImpl.this) { + suspendHandler = handler; + } + return this; + } + + @Override + public CommandProcess resumeHandler(Handler handler) { + synchronized (ProcessImpl.this) { + resumeHandler = handler; + } + return this; + } + + @Override + public CommandProcess endHandler(Handler handler) { + synchronized (ProcessImpl.this) { + endHandler = handler; + } + return this; + } + + @Override + public CommandProcess backgroundHandler(Handler handler) { + synchronized (ProcessImpl.this) { + backgroundHandler = handler; + } + return this; + } + + @Override + public CommandProcess foregroundHandler(Handler handler) { + synchronized (ProcessImpl.this) { + foregroundHandler = handler; + } + return this; + } + + @Override + public void register(int enhanceLock, AdviceListener listener) { + this.enhanceLock = enhanceLock; + AdviceWeaver.reg(enhanceLock, listener); + } + + @Override + public void unregister() { + AdviceWeaver.unReg(enhanceLock); + } + + @Override + public void resume() { + if (this.enhanceLock >= 0 && suspendedListener != null) { + AdviceWeaver.resume(enhanceLock, suspendedListener); + suspendedListener = null; + } + } + + @Override + public void suspend() { + if (this.enhanceLock >= 0) { + suspendedListener = AdviceWeaver.suspend(enhanceLock); + } + } + + @Override + public void end() { + end(0); + } + + @Override + public void end(int statusCode) { + terminate(statusCode, null); + } + } + + static class ProcessOutput { + + private List> stdoutHandlerChain; + private StatisticsFunction statisticsHandler = null; + private List> flushHandlerChain = null; + private String cacheLocation; + private Tty term; + + public ProcessOutput(List> stdoutHandlerChain, String cacheLocation, Tty term) { + // this.stdoutHandlerChain = stdoutHandlerChain; + + int i = 0; + for (; i < stdoutHandlerChain.size(); i++) { + if (stdoutHandlerChain.get(i) instanceof StatisticsFunction) { + break; + } + } + if (i < stdoutHandlerChain.size()) { + this.stdoutHandlerChain = stdoutHandlerChain.subList(0, i + 1); + this.statisticsHandler = (StatisticsFunction) stdoutHandlerChain.get(i); + if (i < stdoutHandlerChain.size() - 1) { + flushHandlerChain = stdoutHandlerChain.subList(i + 1, stdoutHandlerChain.size()); + } + } else { + this.stdoutHandlerChain = stdoutHandlerChain; + } + + this.cacheLocation = cacheLocation; + this.term = term; + } + + private void write(String data) { + if (stdoutHandlerChain != null) { + for (Function function : stdoutHandlerChain) { + data = function.apply(data); + } + } + } + + private void close() { + if (statisticsHandler != null && flushHandlerChain != null) { + String data = statisticsHandler.result(); + + if (flushHandlerChain != null) { + for (Function function : flushHandlerChain) { + data = function.apply(data); + if (function instanceof StatisticsFunction) { + data = ((StatisticsFunction) function).result(); + } + } + } + } + + if (stdoutHandlerChain != null) { + for (Function function : stdoutHandlerChain) { + if (function instanceof CloseFunction) { + ((CloseFunction) function).close(); + } + } + } + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/term/SignalHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/term/SignalHandler.java new file mode 100644 index 00000000..979b599b --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/term/SignalHandler.java @@ -0,0 +1,8 @@ +package com.taobao.arthas.core.shell.term; + +/** + * @author Julien Viet + */ +public interface SignalHandler { + boolean deliver(int key); +} \ No newline at end of file diff --git a/core/src/main/java/com/taobao/arthas/core/shell/term/Term.java b/core/src/main/java/com/taobao/arthas/core/shell/term/Term.java new file mode 100644 index 00000000..ae26da28 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/term/Term.java @@ -0,0 +1,92 @@ +package com.taobao.arthas.core.shell.term; + +import com.taobao.arthas.core.shell.cli.Completion; +import com.taobao.arthas.core.shell.handlers.Handler; +import com.taobao.arthas.core.shell.session.Session; +import io.termd.core.function.Function; + +/** + * The terminal. + * + * @author Julien Viet + */ +public interface Term extends Tty { + + @Override + Term resizehandler(Handler handler); + + @Override + Term stdinHandler(Handler handler); + + Term stdoutHandler(Function handler); + + @Override + Term write(String data); + + /** + * @return the last time this term received input + */ + long lastAccessedTime(); + + /** + * Echo some text in the terminal, escaped if necessary.

+ * + * @param text the text to echo + * @return a reference to this, so the API can be used fluently + */ + Term echo(String text); + + /** + * Associate the term with a session. + * + * @param session the session to set + * @return a reference to this, so the API can be used fluently + */ + Term setSession(Session session); + + /** + * Set an interrupt signal handler on the term. + * + * @param handler the interrupt handler + * @return a reference to this, so the API can be used fluently + */ + Term interruptHandler(SignalHandler handler); + + /** + * Set a suspend signal handler on the term. + * + * @param handler the suspend handler + * @return a reference to this, so the API can be used fluently + */ + Term suspendHandler(SignalHandler handler); + + /** + * Prompt the user a line of text. + * + * @param prompt the displayed prompt + * @param lineHandler the line handler called with the line + */ + void readline(String prompt, Handler lineHandler); + + /** + * Prompt the user a line of text, providing a completion handler to handle user's completion. + * + * @param prompt the displayed prompt + * @param lineHandler the line handler called with the line + * @param completionHandler the completion handler + */ + void readline(String prompt, Handler lineHandler, Handler completionHandler); + + /** + * Set a handler that will be called when the terminal is closed. + * + * @param handler the handler + * @return a reference to this, so the API can be used fluently + */ + Term closeHandler(Handler handler); + + /** + * Close the connection to terminal. + */ + void close(); +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/term/TermServer.java b/core/src/main/java/com/taobao/arthas/core/shell/term/TermServer.java new file mode 100644 index 00000000..af5ab912 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/term/TermServer.java @@ -0,0 +1,84 @@ +package com.taobao.arthas.core.shell.term; + +import com.taobao.arthas.core.config.Configure; +import com.taobao.arthas.core.shell.ShellServerOptions; +import com.taobao.arthas.core.shell.future.Future; +import com.taobao.arthas.core.shell.handlers.Handler; +import com.taobao.arthas.core.shell.term.impl.TelnetTermServer; + +/** + * A server for terminal based applications. + * + * @author Julien Viet + */ +public abstract class TermServer { + + /** + * Create a term server for the Telnet protocol. + * + * @param configure + * @return the term server + */ + public static TermServer createTelnetTermServer(Configure configure, ShellServerOptions options) { + return new TelnetTermServer(configure.getIp(), configure.getTelnetPort(), options.getConnectionTimeout()); + } + + /** + * Create a term server for the HTTP protocol, using an existing router. + * + * @return the term server + */ + public static TermServer createHttpTermServer() { + // TODO + return null; + } + + /** + * Set the term handler that will receive incoming client connections. When a remote terminal connects + * the {@code handler} will be called with the {@link Term} which can be used to interact with the remote + * terminal. + * + * @param handler the term handler + * @return this object + */ + public abstract TermServer termHandler(Handler handler); + + /** + * Bind the term server, the {@link #termHandler(Handler)} must be set before. + * + * @return this object + */ + public TermServer listen() { + return listen(null); + } + + /** + * Bind the term server, the {@link #termHandler(Handler)} must be set before. + * + * @param listenHandler the listen handler + * @return this object + */ + public abstract TermServer listen(Handler> listenHandler); + + /** + * The actual port the server is listening on. This is useful if you bound the server specifying 0 as port number + * signifying an ephemeral port + * + * @return the actual port the server is listening on. + */ + public abstract int actualPort(); + + /** + * Close the server. This will close any currently open connections. The close may not complete until after this + * method has returned. + */ + public abstract void close(); + + /** + * Like {@link #close} but supplying a handler that will be notified when close is complete. + * + * @param completionHandler the handler to be notified when the term server is closed + */ + public abstract void close(Handler> completionHandler); + +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/term/Tty.java b/core/src/main/java/com/taobao/arthas/core/shell/term/Tty.java new file mode 100644 index 00000000..ed5265ee --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/term/Tty.java @@ -0,0 +1,52 @@ +package com.taobao.arthas.core.shell.term; + +import com.taobao.arthas.core.shell.handlers.Handler; + +/** + * Provide interactions with the Shell TTY. + * + * @author Julien Viet + */ +public interface Tty { + + /** + * @return the declared tty type, for instance {@literal vt100}, {@literal xterm-256}, etc... it can be null + * when the tty does not have declared its type. + */ + String type(); + + /** + * @return the current width, i.e the number of rows or {@literal -1} if unknown + */ + int width(); + + /** + * @return the current height, i.e the number of columns or {@literal -1} if unknown + */ + int height(); + + /** + * Set a stream handler on the standard input to read the data. + * + * @param handler the standard input + * @return this object + */ + Tty stdinHandler(Handler handler); + + /** + * Write data to the standard output. + * + * @param data the data to write + * @return this object + */ + Tty write(String data); + + /** + * Set a resize handler, the handler is called when the tty size changes. + * + * @param handler the resize handler + * @return this object + */ + Tty resizehandler(Handler handler); + +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/term/impl/CompletionAdaptor.java b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/CompletionAdaptor.java new file mode 100644 index 00000000..17380f05 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/CompletionAdaptor.java @@ -0,0 +1,73 @@ +package com.taobao.arthas.core.shell.term.impl; + +import com.taobao.arthas.core.shell.cli.CliToken; +import com.taobao.arthas.core.shell.cli.Completion; +import com.taobao.arthas.core.shell.cli.CompletionUtils; +import com.taobao.arthas.core.shell.session.Session; + +import java.util.LinkedList; +import java.util.List; + +/** + * @author beiwei30 on 23/11/2016. + */ +class CompletionAdaptor implements Completion { + private final Session session; + private final String line; + private final List tokens; + private final io.termd.core.readline.Completion completion; + + public CompletionAdaptor(String line, List tokens, io.termd.core.readline.Completion completion, + Session session) { + this.line = line; + this.tokens = tokens; + this.completion = completion; + this.session = session; + } + + @Override + public Session session() { + return session; + } + + @Override + public String rawLine() { + return line; + } + + @Override + public List lineTokens() { + return tokens; + } + + @Override + public void complete(List candidates) { + if (candidates.size() > 1) { + // complete common prefix + String commonPrefix = CompletionUtils.findLongestCommonPrefix(candidates); + if (commonPrefix.length() > 0) { + CliToken lastToken = tokens.get(tokens.size() - 1); + if (!commonPrefix.equals(lastToken.value())) { + // only complete if the common prefix is longer than the last token + String strToComplete = commonPrefix.substring(lastToken.value().length()); + completion.complete(io.termd.core.util.Helper.toCodePoints(strToComplete), false); + return; + } + } + } + if (candidates.size() > 0) { + List suggestions = new LinkedList(); + for (String candidate : candidates) { + suggestions.add(io.termd.core.util.Helper.toCodePoints(candidate)); + } + completion.suggest(suggestions); + } else { + completion.end(); + } + } + + @Override + public void complete(String value, boolean terminal) { + completion.complete(io.termd.core.util.Helper.toCodePoints(value), terminal); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/term/impl/CompletionHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/CompletionHandler.java new file mode 100644 index 00000000..1323d04a --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/CompletionHandler.java @@ -0,0 +1,32 @@ +package com.taobao.arthas.core.shell.term.impl; + +import com.taobao.arthas.core.shell.cli.CliToken; +import com.taobao.arthas.core.shell.cli.CliTokens; +import com.taobao.arthas.core.shell.handlers.Handler; +import com.taobao.arthas.core.shell.session.Session; +import io.termd.core.function.Consumer; +import io.termd.core.readline.Completion; + +import java.util.Collections; +import java.util.List; + +/** + * @author beiwei30 on 23/11/2016. + */ +class CompletionHandler implements Consumer { + private final Handler completionHandler; + private final Session session; + + public CompletionHandler(Handler completionHandler, Session session) { + this.completionHandler = completionHandler; + this.session = session; + } + + @Override + public void accept(final Completion completion) { + final String line = io.termd.core.util.Helper.fromCodePoints(completion.line()); + final List tokens = Collections.unmodifiableList(CliTokens.tokenize(line)); + com.taobao.arthas.core.shell.cli.Completion comp = new CompletionAdaptor(line, tokens, completion, session); + completionHandler.handle(comp); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/term/impl/Helper.java b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/Helper.java new file mode 100644 index 00000000..1f3b369c --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/Helper.java @@ -0,0 +1,80 @@ +package com.taobao.arthas.core.shell.term.impl; + +import com.taobao.arthas.core.shell.ShellServerOptions; +import com.taobao.arthas.core.shell.term.TermServer; +import com.taobao.arthas.core.util.LogUtil; +import com.taobao.middleware.logger.Logger; +import io.termd.core.readline.Keymap; + +import java.io.FileInputStream; +import java.io.InputStream; + +/** + * @author Julien Viet + */ +public class Helper { + + private static Logger logger = LogUtil.getArthasLogger(); + + public static Keymap loadKeymap() { + return new Keymap(loadInputRcFile()); + } + + public static InputStream loadInputRcFile() { + InputStream inputrc; + // step 1: load custom keymap file + try { + String customInputrc = System.getProperty("user.home") + "/.arthas/conf/inputrc"; + inputrc = new FileInputStream(customInputrc); + logger.info("Loaded custom keymap file from " + customInputrc); + return inputrc; + } catch (Throwable e) { + // ignore + } + + // step 2: load arthas default keymap file + inputrc = TermServer.class.getClassLoader().getResourceAsStream(ShellServerOptions.DEFAULT_INPUTRC); + if (inputrc != null) { + logger.info("Loaded arthas keymap file from " + ShellServerOptions.DEFAULT_INPUTRC); + return inputrc; + } + + // step 3: fall back to termd default keymap file + inputrc = Keymap.class.getResourceAsStream("inputrc"); + if (inputrc != null) { + return inputrc; + } + + throw new IllegalStateException("Could not load inputrc file."); + } + + +// public static Buffer loadResource(FileSystem fs, String path) { +// try { +// return fs.readFileBlocking(path); +// } catch (Exception e) { +// return loadResource(path); +// } +// } + +// public static Buffer loadResource(String path) { +// URL resource = HttpTermServer.class.getResource(path); +// if (resource != null) { +// try { +// byte[] tmp = new byte[512]; +// InputStream in = resource.openStream(); +// Buffer buffer = Buffer.buffer(); +// while (true) { +// int l = in.read(tmp); +// if (l == -1) { +// break; +// } +// buffer.appendBytes(tmp, 0, l); +// } +// return buffer; +// } catch (IOException ignore) { +// } +// } +// return null; +// } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/term/impl/HttpTermServer.java b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/HttpTermServer.java new file mode 100644 index 00000000..9caf07aa --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/HttpTermServer.java @@ -0,0 +1,82 @@ +package com.taobao.arthas.core.shell.term.impl; + +import com.taobao.arthas.core.shell.future.Future; +import com.taobao.arthas.core.shell.handlers.Handler; +import com.taobao.arthas.core.shell.term.Term; +import com.taobao.arthas.core.shell.term.TermServer; +import com.taobao.arthas.core.util.LogUtil; +import com.taobao.middleware.logger.Logger; +import io.termd.core.function.Consumer; +import io.termd.core.http.netty.NettyWebsocketTtyBootstrap; +import io.termd.core.tty.TtyConnection; + +import java.util.concurrent.TimeUnit; + +/** + * @author beiwei30 on 18/11/2016. + */ +public class HttpTermServer extends TermServer { + + private static Logger logger = LogUtil.getArthasLogger(); + + private Handler termHandler; + private NettyWebsocketTtyBootstrap bootstrap; + private String hostIp; + private int port; + private long connectionTimeout; + + public HttpTermServer(String hostIp, int port, long connectionTimeout) { + this.hostIp = hostIp; + this.port = port; + this.connectionTimeout = connectionTimeout; + } + + @Override + public TermServer termHandler(Handler handler) { + this.termHandler = handler; + return this; + } + + @Override + public TermServer listen(Handler> listenHandler) { + // TODO: charset and inputrc from options + bootstrap = new NettyWebsocketTtyBootstrap().setHost(hostIp).setPort(port); + try { + bootstrap.start(new Consumer() { + @Override + public void accept(final TtyConnection conn) { + termHandler.handle(new TermImpl(Helper.loadKeymap(), conn)); + } + }).get(connectionTimeout, TimeUnit.MILLISECONDS); + listenHandler.handle(Future.succeededFuture()); + } catch (Throwable t) { + logger.error(null, "Error listening to port " + port, t); + listenHandler.handle(Future.failedFuture(t)); + } + return this; + } + + @Override + public int actualPort() { + return bootstrap.getPort(); + } + + @Override + public void close() { + close(null); + } + + @Override + public void close(Handler> completionHandler) { + if (bootstrap != null) { + bootstrap.stop(); + if (completionHandler != null) { + completionHandler.handle(Future.succeededFuture()); + } + } else { + if (completionHandler != null) { + completionHandler.handle(Future.failedFuture("telnet term server not started")); + } + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/term/impl/TelnetTermServer.java b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/TelnetTermServer.java new file mode 100644 index 00000000..b18a1461 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/TelnetTermServer.java @@ -0,0 +1,84 @@ +package com.taobao.arthas.core.shell.term.impl; + +import com.taobao.arthas.core.shell.future.Future; +import com.taobao.arthas.core.shell.handlers.Handler; +import com.taobao.arthas.core.shell.term.Term; +import com.taobao.arthas.core.shell.term.TermServer; +import com.taobao.arthas.core.util.LogUtil; +import com.taobao.middleware.logger.Logger; +import io.termd.core.function.Consumer; +import io.termd.core.telnet.netty.NettyTelnetTtyBootstrap; +import io.termd.core.tty.TtyConnection; + +import java.util.concurrent.TimeUnit; + +/** + * Encapsulate the Telnet server setup. + * + * @author Julien Viet + */ +public class TelnetTermServer extends TermServer { + + private static Logger logger = LogUtil.getArthasLogger(); + + private NettyTelnetTtyBootstrap bootstrap; + private String hostIp; + private int port; + private long connectionTimeout; + + private Handler termHandler; + + public TelnetTermServer(String hostIp, int port, long connectionTimeout) { + this.hostIp = hostIp; + this.port = port; + this.connectionTimeout = connectionTimeout; + } + + @Override + public TermServer termHandler(Handler handler) { + termHandler = handler; + return this; + } + + @Override + public TermServer listen(Handler> listenHandler) { + // TODO: charset and inputrc from options + bootstrap = new NettyTelnetTtyBootstrap().setHost(hostIp).setPort(port); + try { + bootstrap.start(new Consumer() { + @Override + public void accept(final TtyConnection conn) { + termHandler.handle(new TermImpl(Helper.loadKeymap(), conn)); + } + }).get(connectionTimeout, TimeUnit.MILLISECONDS); + listenHandler.handle(Future.succeededFuture()); + } catch (Throwable t) { + logger.error(null, "Error listening to port " + port, t); + listenHandler.handle(Future.failedFuture(t)); + } + return this; + } + + @Override + public void close() { + close(null); + } + + @Override + public void close(Handler> completionHandler) { + if (bootstrap != null) { + bootstrap.stop(); + if (completionHandler != null) { + completionHandler.handle(Future.succeededFuture()); + } + } else { + if (completionHandler != null) { + completionHandler.handle(Future.failedFuture("telnet term server not started")); + } + } + } + + public int actualPort() { + return bootstrap.getPort(); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/term/impl/TermImpl.java b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/TermImpl.java new file mode 100644 index 00000000..34731be5 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/TermImpl.java @@ -0,0 +1,246 @@ +package com.taobao.arthas.core.shell.term.impl; + +import com.taobao.arthas.core.shell.cli.Completion; +import com.taobao.arthas.core.shell.handlers.term.CloseHandlerWrapper; +import com.taobao.arthas.core.shell.handlers.term.DefaultTermStdinHandler; +import com.taobao.arthas.core.shell.handlers.term.EventHandler; +import com.taobao.arthas.core.shell.handlers.Handler; +import com.taobao.arthas.core.shell.handlers.term.RequestHandler; +import com.taobao.arthas.core.shell.handlers.term.SizeHandlerWrapper; +import com.taobao.arthas.core.shell.handlers.term.StdinHandlerWrapper; +import com.taobao.arthas.core.shell.session.Session; +import com.taobao.arthas.core.shell.term.SignalHandler; +import com.taobao.arthas.core.shell.term.Term; +import com.taobao.arthas.core.util.Constants; +import com.taobao.arthas.core.util.FileUtils; +import io.termd.core.function.Consumer; +import io.termd.core.readline.Function; +import io.termd.core.readline.Keymap; +import io.termd.core.readline.Readline; +import io.termd.core.tty.TtyConnection; +import io.termd.core.util.Helper; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Julien Viet + */ +public class TermImpl implements Term { + + private static final List readlineFunctions = Helper.loadServices(Function.class.getClassLoader(), Function.class); + + private Readline readline; + private Consumer echoHandler; + private TtyConnection conn; + private volatile Handler stdinHandler; + private List> stdoutHandlerChain; + private SignalHandler interruptHandler; + private SignalHandler suspendHandler; + private Session session; + private boolean inReadline; + + public TermImpl(TtyConnection conn) { + this(com.taobao.arthas.core.shell.term.impl.Helper.loadKeymap(), conn); + } + + public TermImpl(Keymap keymap, TtyConnection conn) { + this.conn = conn; + readline = new Readline(keymap); + readline.setHistory(FileUtils.loadCommandHistory(new File(Constants.CMD_HISTORY_FILE))); + for (Function function : readlineFunctions) { + readline.addFunction(function); + } + + echoHandler = new DefaultTermStdinHandler(this); + conn.setStdinHandler(echoHandler); + conn.setEventHandler(new EventHandler(this)); + } + + @Override + public Term setSession(Session session) { + this.session = session; + return this; + } + + @Override + public void readline(String prompt, Handler lineHandler) { + if (conn.getStdinHandler() != echoHandler) { + throw new IllegalStateException(); + } + if (inReadline) { + throw new IllegalStateException(); + } + inReadline = true; + readline.readline(conn, prompt, new RequestHandler(this, lineHandler)); + } + + public void readline(String prompt, Handler lineHandler, Handler completionHandler) { + if (conn.getStdinHandler() != echoHandler) { + throw new IllegalStateException(); + } + if (inReadline) { + throw new IllegalStateException(); + } + inReadline = true; + readline.readline(conn, prompt, new RequestHandler(this, lineHandler), new CompletionHandler(completionHandler, session)); + } + + @Override + public Term closeHandler(final Handler handler) { + if (handler != null) { + conn.setCloseHandler(new CloseHandlerWrapper(handler)); + } else { + conn.setCloseHandler(null); + } + return this; + } + + public long lastAccessedTime() { + return conn.lastAccessedTime(); + } + + @Override + public String type() { + return conn.terminalType(); + } + + @Override + public int width() { + return conn.size() != null ? conn.size().x() : -1; + } + + @Override + public int height() { + return conn.size() != null ? conn.size().y() : -1; + } + + void checkPending() { + if (stdinHandler != null && readline.hasEvent()) { + stdinHandler.handle(Helper.fromCodePoints(readline.nextEvent().buffer().array())); + checkPending(); + } + } + + @Override + public TermImpl resizehandler(Handler handler) { + if (inReadline) { + throw new IllegalStateException(); + } + if (handler != null) { + conn.setSizeHandler(new SizeHandlerWrapper(handler)); + } else { + conn.setSizeHandler(null); + } + return this; + } + + @Override + public Term stdinHandler(final Handler handler) { + if (inReadline) { + throw new IllegalStateException(); + } + stdinHandler = handler; + if (handler != null) { + conn.setStdinHandler(new StdinHandlerWrapper(handler)); + checkPending(); + } else { + conn.setStdinHandler(echoHandler); + } + return this; + } + + @Override + public Term stdoutHandler(io.termd.core.function.Function handler) { + if (stdoutHandlerChain == null) { + stdoutHandlerChain = new ArrayList>(); + } + stdoutHandlerChain.add(handler); + return this; + } + + @Override + public Term write(String data) { + if (stdoutHandlerChain != null) { + for (io.termd.core.function.Function function : stdoutHandlerChain) { + data = function.apply(data); + } + } + conn.write(data); + return this; + } + + public TermImpl interruptHandler(SignalHandler handler) { + interruptHandler = handler; + return this; + } + + public TermImpl suspendHandler(SignalHandler handler) { + suspendHandler = handler; + return this; + } + + public void close() { + conn.close(); + FileUtils.saveCommandHistory(readline.getHistory(), new File(Constants.CMD_HISTORY_FILE)); + } + + public TermImpl echo(String text) { + echo(Helper.toCodePoints(text)); + return this; + } + + public void setInReadline(boolean inReadline) { + this.inReadline = inReadline; + } + + public Readline getReadline() { + return readline; + } + + public void handleIntr(Integer key) { + if (interruptHandler == null || !interruptHandler.deliver(key)) { + echo(key, '\n'); + } + } + + public void handleEof(Integer key) { + // Pseudo signal + if (stdinHandler != null) { + stdinHandler.handle(Helper.fromCodePoints(new int[]{key})); + } else { + echo(key); + readline.queueEvent(new int[]{key}); + } + } + + public void handleSusp(Integer key) { + if (suspendHandler == null || !suspendHandler.deliver(key)) { + echo(key, 'Z' - 64); + } + } + + public void echo(int... codePoints) { + Consumer out = conn.stdoutHandler(); + for (int codePoint : codePoints) { + if (codePoint < 32) { + if (codePoint == '\t') { + out.accept(new int[]{'\t'}); + } else if (codePoint == '\b') { + out.accept(new int[]{'\b', ' ', '\b'}); + } else if (codePoint == '\r' || codePoint == '\n') { + out.accept(new int[]{'\n'}); + } else { + out.accept(new int[]{'^', codePoint + 64}); + } + } else { + if (codePoint == 127) { + out.accept(new int[]{'\b', ' ', '\b'}); + } else { + out.accept(new int[]{codePoint}); + } + } + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/ArrayUtils.java b/core/src/main/java/com/taobao/arthas/core/util/ArrayUtils.java new file mode 100644 index 00000000..c73bb82a --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/ArrayUtils.java @@ -0,0 +1,34 @@ +package com.taobao.arthas.core.util; + +/** + * @author ralf0131 2016-12-28 14:57. + */ +public class ArrayUtils { + + /** + * An empty immutable {@code long} array. + */ + public static final long[] EMPTY_LONG_ARRAY = new long[0]; + + /** + *

Converts an array of object Longs to primitives.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Long} array, may be {@code null} + * @return a {@code long} array, {@code null} if null array input + * @throws NullPointerException if array content is {@code null} + */ + public static long[] toPrimitive(final Long[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_LONG_ARRAY; + } + final long[] result = new long[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i].longValue(); + } + return result; + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/ArthasBanner.java b/core/src/main/java/com/taobao/arthas/core/util/ArthasBanner.java new file mode 100644 index 00000000..1afd9c14 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/ArthasBanner.java @@ -0,0 +1,85 @@ +package com.taobao.arthas.core.util; + +import com.taobao.arthas.core.shell.ShellServerOptions; +import com.taobao.middleware.logger.Logger; +import com.taobao.text.Color; +import com.taobao.text.Decoration; +import com.taobao.text.ui.TableElement; +import com.taobao.text.util.RenderUtil; + +import java.io.IOException; + +import static com.taobao.text.ui.Element.label; + +/** + * @author beiwei30 on 16/11/2016. + */ +public class ArthasBanner { + private static final String LOGO_LOCATION = "/com/taobao/arthas/core/res/logo.txt"; + private static final String CREDIT_LOCATION = "/com/taobao/arthas/core/res/thanks.txt"; + private static final String VERSION_LOCATION = "/com/taobao/arthas/core/res/version"; + private static final String WIKI = "middleware-container/arthas/wikis/home"; + + private static String LOGO = "Welcome to Arthas"; + private static String VERSION = "unknown"; + private static String THANKS = ""; + + private static final Logger logger = LogUtil.getArthasLogger(); + + static { + try { + String logoText = IOUtils.toString(ShellServerOptions.class.getResourceAsStream(LOGO_LOCATION)); + THANKS = IOUtils.toString(ShellServerOptions.class.getResourceAsStream(CREDIT_LOCATION)); + VERSION = IOUtils.toString(ShellServerOptions.class.getResourceAsStream(VERSION_LOCATION)); + + StringBuilder sb = new StringBuilder(); + String[] LOGOS = new String[6]; + int i = 0, j = 0; + for (String line : logoText.split("\n")) { + sb.append(line); + sb.append("\n"); + if (i++ == 4) { + LOGOS[j++] = sb.toString(); + i = 0; + sb.setLength(0); + } + } + + TableElement logoTable = new TableElement(); + logoTable.row(label(LOGOS[0]).style(Decoration.bold.fg(Color.red)), + label(LOGOS[1]).style(Decoration.bold.fg(Color.yellow)), + label(LOGOS[2]).style(Decoration.bold.fg(Color.cyan)), + label(LOGOS[3]).style(Decoration.bold.fg(Color.magenta)), + label(LOGOS[4]).style(Decoration.bold.fg(Color.green)), + label(LOGOS[5]).style(Decoration.bold.fg(Color.blue))); + LOGO = RenderUtil.render(logoTable); + } catch (IOException ioe) { + ioe.printStackTrace(); + } + } + + public static String wiki() { + return WIKI; + } + + public static String credit() { + return THANKS; + } + + public static String version() { + return VERSION; + } + + public static String logo() { + return LOGO; + } + + public static String plainTextLogo() { + return RenderUtil.ansiToPlainText(LOGO); + } + + public static String welcome() { + logger.info("arthas version: " + version()); + return logo() + "\n" + "wiki: " + wiki() + "\n" + "version: " + version(); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/ArthasCheckUtils.java b/core/src/main/java/com/taobao/arthas/core/util/ArthasCheckUtils.java new file mode 100644 index 00000000..f540ba49 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/ArthasCheckUtils.java @@ -0,0 +1,57 @@ +package com.taobao.arthas.core.util; + +/** + * 检查工具类 + * Created by vlinux on 15/5/19. + */ +public class ArthasCheckUtils { + + /** + * 比对某个元素是否在集合中
+ * + * @param e 元素 + * @param s 元素集合 + * @param 元素类型 + * @return
+ * (1,1,2,3) == true + * (1,2,3,4) == false + * (null,1,null,2) == true + * (1,null) == false + */ + public static boolean isIn(E e, E... s) { + + if (null != s) { + for (E es : s) { + if (isEquals(e, es)) { + return true; + } + } + } + + return false; + + } + + /** + * 比对两个对象是否相等
+ * + * @param src 源对象 + * @param target 目标对象 + * @param 对象类型 + * @return
+ * (null, null) == true + * (1L,2L) == false + * (1L,1L) == true + * ("abc",null) == false + * (null,"abc") == false + */ + public static boolean isEquals(E src, E target) { + + return null == src + && null == target + || null != src + && null != target + && src.equals(target); + + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/Constants.java b/core/src/main/java/com/taobao/arthas/core/util/Constants.java new file mode 100644 index 00000000..ad44cd31 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/Constants.java @@ -0,0 +1,56 @@ +package com.taobao.arthas.core.util; + +import java.io.File; +import java.lang.management.ManagementFactory; + +import com.taobao.arthas.core.view.Ansi; + +/** + * @author ralf0131 2016-12-28 16:20. + */ +public interface Constants { + + /** + * Spy的全类名 + */ + String SPY_CLASSNAME = "java.arthas.Spy"; + + /** + * 中断提示 + */ + String ABORT_MSG = "Press Ctrl+C to abort."; + + /** + * 空字符串 + */ + String EMPTY_STRING = ""; + + /** + * 命令提示符 + */ + String DEFAULT_PROMPT = "$ "; + + /** + * 带颜色命令提示符 + * raw string: "[33m$ " + */ + String COLOR_PROMPT = Ansi.ansi().fg(Ansi.Color.YELLOW).a(DEFAULT_PROMPT).reset().toString(); + + /** + * 方法执行耗时 + */ + String COST_VARIABLE = "cost"; + + String CMD_HISTORY_FILE = System.getProperty("user.home") + File.separator + ".arthas" + File.separator + "history"; + + /** + * 当前进程PID + */ + String PID = ManagementFactory.getRuntimeMXBean().getName().split("@")[0]; + + /** + * 缓存目录 + */ + String CACHE_ROOT = System.getProperty("user.home") + File.separator + "logs" + File.separator + "arthas-cache"; + +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/DateUtils.java b/core/src/main/java/com/taobao/arthas/core/util/DateUtils.java new file mode 100644 index 00000000..2749911c --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/DateUtils.java @@ -0,0 +1,22 @@ +package com.taobao.arthas.core.util; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * @author diecui1202 on 2017/10/25. + */ +public class DateUtils { + + private static final ThreadLocal dataFormat = new ThreadLocal() { + + @Override + protected SimpleDateFormat initialValue() { + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + } + }; + + public static String getCurrentDate() { + return dataFormat.get().format(new Date()); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/FileUtils.java b/core/src/main/java/com/taobao/arthas/core/util/FileUtils.java new file mode 100644 index 00000000..1c321efb --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/FileUtils.java @@ -0,0 +1,167 @@ +package com.taobao.arthas.core.util; + +/** + * Copied from {@link org.apache.commons.io.FileUtils} + * @author ralf0131 2016-12-28 11:46. + */ +import io.termd.core.util.Helper; + +import java.io.*; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +public class FileUtils { + + /** + * Writes a byte array to a file creating the file if it does not exist. + *

+ * NOTE: As from v1.3, the parent directories of the file will be created + * if they do not exist. + * + * @param file the file to write to + * @param data the content to write to the file + * @throws IOException in case of an I/O error + * @since 1.1 + */ + public static void writeByteArrayToFile(File file, byte[] data) throws IOException { + writeByteArrayToFile(file, data, false); + } + + /** + * Writes a byte array to a file creating the file if it does not exist. + * + * @param file the file to write to + * @param data the content to write to the file + * @param append if {@code true}, then bytes will be added to the + * end of the file rather than overwriting + * @throws IOException in case of an I/O error + * @since IO 2.1 + */ + public static void writeByteArrayToFile(File file, byte[] data, boolean append) throws IOException { + OutputStream out = null; + try { + out = openOutputStream(file, append); + out.write(data); + out.close(); // don't swallow close Exception if copy completes normally + } finally { + try { + if (out != null) { + out.close(); + } + } catch (IOException ioe) { + // ignore + } + } + } + + /** + * Opens a {@link FileOutputStream} for the specified file, checking and + * creating the parent directory if it does not exist. + *

+ * At the end of the method either the stream will be successfully opened, + * or an exception will have been thrown. + *

+ * The parent directory will be created if it does not exist. + * The file will be created if it does not exist. + * An exception is thrown if the file object exists but is a directory. + * An exception is thrown if the file exists but cannot be written to. + * An exception is thrown if the parent directory cannot be created. + * + * @param file the file to open for output, must not be {@code null} + * @param append if {@code true}, then bytes will be added to the + * end of the file rather than overwriting + * @return a new {@link FileOutputStream} for the specified file + * @throws IOException if the file object is a directory + * @throws IOException if the file cannot be written to + * @throws IOException if a parent directory needs creating but that fails + * @since 2.1 + */ + public static FileOutputStream openOutputStream(File file, boolean append) throws IOException { + if (file.exists()) { + if (file.isDirectory()) { + throw new IOException("File '" + file + "' exists but is a directory"); + } + if (!file.canWrite()) { + throw new IOException("File '" + file + "' cannot be written to"); + } + } else { + File parent = file.getParentFile(); + if (parent != null) { + if (!parent.mkdirs() && !parent.isDirectory()) { + throw new IOException("Directory '" + parent + "' could not be created"); + } + } + } + return new FileOutputStream(file, append); + } + + /** + * save the command history to the given file, data will be overridden. + * @param history the command history, each represented by an int array + * @param file the file to save the history + */ + public static void saveCommandHistory(List history, File file) { + OutputStream out = null; + try { + out = openOutputStream(file, false); + for (int[] command: history) { + for (int i : command) { + out.write(i); + } + out.write('\n'); + } + } catch (IOException e) { + // ignore + } finally { + try { + if (out != null) { + out.close(); + } + } catch (IOException ioe) { + // ignore + } + } + } + + public static List loadCommandHistory(File file) { + BufferedReader br = null; + List history = new ArrayList(); + try { + br = new BufferedReader(new InputStreamReader(new FileInputStream(file))); + String line; + while ((line = br.readLine()) != null) { + history.add(Helper.toCodePoints(line)); + } + } catch (IOException e) { + // ignore + } finally { + try { + if (br != null) { + br.close(); + } + } catch (IOException ioe) { + // ignore + } + } + return history; + } + + public static String readFileToString(File file, Charset encoding) throws IOException { + FileInputStream stream = new FileInputStream(file); + try { + Reader reader = new BufferedReader(new InputStreamReader(stream, encoding)); + StringBuilder builder = new StringBuilder(); + char[] buffer = new char[8192]; + int read; + while ((read = reader.read(buffer, 0, buffer.length)) > 0) { + builder.append(buffer, 0, read); + } + return builder.toString(); + } finally { + stream.close(); + } + } + +} + diff --git a/core/src/main/java/com/taobao/arthas/core/util/IOUtils.java b/core/src/main/java/com/taobao/arthas/core/util/IOUtils.java new file mode 100644 index 00000000..87bcad08 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/IOUtils.java @@ -0,0 +1,152 @@ +package com.taobao.arthas.core.util; + +/** + * Copied from {@link org.apache.commons.io.IOUtils} + * @author ralf0131 2016-12-28 11:41. + */ + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; + +public class IOUtils { + + private static final int EOF = -1; + + /** + * The default buffer size ({@value}) to use for + * {@link #copyLarge(InputStream, OutputStream)} + */ + private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; + + /** + * Get the contents of an InputStream as a byte[]. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param input the InputStream to read from + * @return the requested byte array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + */ + public static byte[] toByteArray(InputStream input) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + copy(input, output); + return output.toByteArray(); + } + + + /** + * Copy bytes from an InputStream to an + * OutputStream. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

+ * Large streams (over 2GB) will return a bytes copied value of + * -1 after the copy has completed since the correct + * number of bytes cannot be returned as an int. For large streams + * use the copyLarge(InputStream, OutputStream) method. + * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @return the number of bytes copied, or -1 if > Integer.MAX_VALUE + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 1.1 + */ + public static int copy(InputStream input, OutputStream output) throws IOException { + long count = copyLarge(input, output); + if (count > Integer.MAX_VALUE) { + return -1; + } + return (int) count; + } + + /** + * Copy bytes from a large (over 2GB) InputStream to an + * OutputStream. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

+ * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}. + * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @return the number of bytes copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 1.3 + */ + public static long copyLarge(InputStream input, OutputStream output) + throws IOException { + return copyLarge(input, output, new byte[DEFAULT_BUFFER_SIZE]); + } + + /** + * Copy bytes from a large (over 2GB) InputStream to an + * OutputStream. + *

+ * This method uses the provided buffer, so there is no need to use a + * BufferedInputStream. + *

+ * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @param buffer the buffer to use for the copy + * @return the number of bytes copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.2 + */ + public static long copyLarge(InputStream input, OutputStream output, byte[] buffer) + throws IOException { + long count = 0; + int n = 0; + while (EOF != (n = input.read(buffer))) { + output.write(buffer, 0, n); + count += n; + } + return count; + } + + /** + * Get the contents of an InputStream as a String + * using the default character encoding of the platform. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param input the InputStream to read from + * @return the requested String + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + */ + public static String toString(InputStream input) throws IOException { + BufferedReader br = null; + try { + StringBuilder sb = new StringBuilder(); + br = new BufferedReader(new InputStreamReader(input)); + String line; + while ((line = br.readLine()) != null) { + sb.append(line).append("\n"); + } + return sb.toString(); + } finally { + if (br != null) { + try { + br.close(); + } catch (IOException e) { + // ignore + } + } + } + } + + +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/IPUtils.java b/core/src/main/java/com/taobao/arthas/core/util/IPUtils.java new file mode 100644 index 00000000..1f7508b2 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/IPUtils.java @@ -0,0 +1,77 @@ +package com.taobao.arthas.core.util; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.util.Enumeration; + +/** + * @author weipeng2k 2015年1月30日 下午3:06:47 + */ +public class IPUtils { + + /** + * 判断当前操作是否Windows. + * + * @return true---是Windows操作系统 + */ + public static boolean isWindowsOS() { + boolean isWindowsOS = false; + String osName = System.getProperty("os.name"); + if (osName.toLowerCase().indexOf("windows") > -1) { + isWindowsOS = true; + } + return isWindowsOS; + } + + /** + * 获取本机IP地址,并自动区分Windows还是Linux操作系统 + * + * @return String + */ + public static String getLocalIP() { + String sIP = null; + InetAddress ip = null; + try { + if (isWindowsOS()) { + ip = InetAddress.getLocalHost(); + } else { + // 如果是回环地址则扫描所有的NetWorkInterface + if (!InetAddress.getLocalHost().isLoopbackAddress()) { + ip = InetAddress.getLocalHost(); + } else { + boolean bFindIP = false; + Enumeration netInterfaces = (Enumeration) NetworkInterface.getNetworkInterfaces(); + while (netInterfaces.hasMoreElements()) { + if (bFindIP) { + break; + } + NetworkInterface ni = (NetworkInterface) netInterfaces.nextElement(); + // ----------特定情况,可以考虑用ni.getName判断 + // 遍历所有ip + Enumeration ips = ni.getInetAddresses(); + while (ips.hasMoreElements()) { + ip = (InetAddress) ips.nextElement(); + // 127.开头的都是lookback地址 + if (ip.isSiteLocalAddress() && !ip.isLoopbackAddress() + && ip.getHostAddress().indexOf(":") == -1) { + bFindIP = true; + break; + } + } + + } + } + } + } catch (Exception e) { + } + + if (ip != null) { + sIP = ip.getHostAddress(); + } + return sIP; + } + + public static void main(String[] args) { + System.out.println(getLocalIP()); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/LogUtil.java b/core/src/main/java/com/taobao/arthas/core/util/LogUtil.java new file mode 100644 index 00000000..f4dc5541 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/LogUtil.java @@ -0,0 +1,84 @@ +package com.taobao.arthas.core.util; + +import com.taobao.middleware.logger.Level; +import com.taobao.middleware.logger.Logger; +import com.taobao.middleware.logger.LoggerFactory; +import com.taobao.middleware.logger.support.LogLog; +import com.taobao.middleware.logger.support.LoggerHelper; + +/** + * Arthas日志 + * Created by vlinux on 15/3/8. + */ +public class LogUtil { + + /** + * Arthas 内部日志Logger + */ + private static final Logger arthasLogger; + + private static final org.slf4j.Logger resultLogger; + + /** + * 接管Netty的Logger + */ + private static final Logger nettyLogger; + + /** + * 接管termd的Logger + */ + private static final Logger termdLogger; + + public static final String LOGGER_FILE = LoggerHelper.getLogFile("arthas", "arthas.log"); + + static { + LogLog.setQuietMode(true); + + LoggerHelper.setPattern("arthas-cache", "%d{yyyy-MM-dd HH:mm:ss.SSS}%n%m%n"); + + arthasLogger = LoggerFactory.getLogger("arthas"); + arthasLogger.activateAppenderWithTimeAndSizeRolling("arthas", "arthas.log", "UTF-8", "100MB"); + arthasLogger.setLevel(Level.INFO); + arthasLogger.setAdditivity(false); + + com.taobao.middleware.logger.Logger log = LoggerFactory.getLogger("result"); + log.activateAppenderWithSizeRolling("arthas-cache", "result.log", "UTF-8", "100MB", 3); + log.setAdditivity(false); + log.activateAsync(64, -1); + resultLogger = (org.slf4j.Logger) log.getDelegate(); + + nettyLogger = LoggerFactory.getLogger("io.netty"); + nettyLogger.activateAppender(arthasLogger); + nettyLogger.setLevel(Level.INFO); + nettyLogger.setAdditivity(false); + + termdLogger = LoggerFactory.getLogger("io.termd"); + termdLogger.activateAppender(arthasLogger); + termdLogger.setLevel(Level.INFO); + termdLogger.setAdditivity(false); + } + + public static Logger getArthasLogger() { + return arthasLogger; + } + + public static org.slf4j.Logger getResultLogger() { + return resultLogger; + } + + public static void closeResultLogger() { + closeSlf4jLogger(resultLogger); + } + + public static void closeSlf4jLogger(org.slf4j.Logger logger) { + if (logger != null) { + if (logger instanceof ch.qos.logback.classic.Logger) { + ((ch.qos.logback.classic.Logger) logger).detachAndStopAllAppenders(); + } else { + // arthas strongly depends on logback. + // So do nothing here + // middleware-container/arthas/issues/123 + } + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/NetUtils.java b/core/src/main/java/com/taobao/arthas/core/util/NetUtils.java new file mode 100644 index 00000000..535b63bb --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/NetUtils.java @@ -0,0 +1,204 @@ +package com.taobao.arthas.core.util; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.HttpURLConnection; +import java.net.Socket; +import java.net.URL; + +/** + * @author ralf0131 on 2015-11-11 15:39. + */ +public class NetUtils { + + private static final String QOS_HOST = "localhost"; + private static final int QOS_PORT = 12201; + private static final String QOS_RESPONSE_START_LINE = "pandora>[QOS Response]"; + private static final int INTERNAL_SERVER_ERROR = 500; + + /** + * This implementation is based on Apache HttpClient. + * @param urlString the requested url + * @return the response string of given url + */ + public static Response request(String urlString) { + HttpURLConnection urlConnection = null; + try { + URL url = new URL(urlString); + urlConnection = (HttpURLConnection)url.openConnection(); + // prefer json to text + urlConnection.setRequestProperty("Accept", "application/json,text/plain;q=0.2"); + BufferedReader br = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); + String line = null; + StringBuilder sb = new StringBuilder(); + while ((line = br.readLine()) != null) { + sb.append(line).append("\n"); + } + int statusCode = urlConnection.getResponseCode(); + String result = sb.toString().trim(); + if (statusCode == INTERNAL_SERVER_ERROR) { + JSONObject errorObj = JSON.parseObject(result); + if (errorObj.containsKey("errorMsg")) { + return new Response(errorObj.getString("errorMsg"), false); + } + return new Response(result, false); + } + return new Response(result); + } catch (IOException e) { + return new Response(e.getMessage(), false); + } finally { + if (urlConnection != null) { + urlConnection.disconnect(); + } + } + } + + /** + * @deprecated + * This implementation is based on HttpURLConnection, + * which can not detail with status code other than 200. + * @param url the requested url + * @return the response string of given url + */ + public static String simpleRequest(String url) { + BufferedReader br = null; + try { + URL obj = new URL(url); + HttpURLConnection con = (HttpURLConnection) obj.openConnection(); + con.setRequestProperty("Accept", "application/json"); + int responseCode = con.getResponseCode(); + + br = new BufferedReader(new InputStreamReader(con.getInputStream())); + StringBuffer sb = new StringBuffer(); + String line = null; + while ((line = br.readLine()) != null) { + sb.append(line); + sb.append("\n"); + } + String result = sb.toString().trim(); + if (responseCode == 500) { + JSONObject errorObj = JSON.parseObject(result); + if (errorObj.containsKey("errorMsg")) { + return errorObj.getString("errorMsg"); + } + return result; + } else { + return result; + } + + } catch (Exception e) { + return null; + } finally { + if (br != null) { + try { + br.close(); + } catch (IOException e) { + // ignore + } + } + } + } + + /** + * Only use this method when tomcat monitor version <= 1.0.1 + * This will send http request to pandora qos port 12201, + * and display the response. + * Note that pandora qos response is not fully HTTP compatible under version 2.1.0, + * so we filtered some of the content and only display useful content. + * @param path the path relative to http://localhost:12201 + * e.g. /pandora/ls + * For commands that requires arguments, use the following format + * e.g. /pandora/find?arg0=RPCProtocolService + * Note that the parameter name is never used in pandora qos, + * so the name(e.g. arg0) is irrelevant. + * @return the qos response in string format + */ + public static Response requestViaSocket(String path) { + BufferedReader br = null; + try { + Socket s = new Socket(QOS_HOST, QOS_PORT); + PrintWriter pw = new PrintWriter(s.getOutputStream()); + pw.println("GET " + path + " HTTP/1.1"); + pw.println("Host: " + QOS_HOST + ":" + QOS_PORT); + pw.println(""); + pw.flush(); + + br = new BufferedReader(new InputStreamReader(s.getInputStream())); + StringBuffer sb = new StringBuffer(); + String line = null; + boolean start = false; + while ((line = br.readLine()) != null) { + if (start) { + sb.append(line).append("\n"); + } + if (line.equals(QOS_RESPONSE_START_LINE)) { + start = true; + } + } + String result = sb.toString().trim(); + return new Response(result); + } catch (Exception e) { + return new Response(e.getMessage(), false); + } finally { + if (br != null) { + try { + br.close(); + } catch (IOException e) { + // ignore + } + } + } + } + + public static class Response { + + private boolean success; + private String content; + + public Response(String content, boolean success) { + this.success = success; + this.content = content; + } + + public Response(String content) { + this.content = content; + this.success = true; + } + + public boolean isSuccess() { + return success; + } + + public String getContent() { + return content; + } + } + + + /** + * Test if a port is open on the give host + */ + public static boolean serverListening(String host, int port) { + Socket s = null; + try { + s = new Socket(host, port); + return true; + } catch (Exception e) { + return false; + } finally { + if (s != null) + try { + s.close(); + } catch (Exception e) { + // ignore + } + } + } + + +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/ObjectUtils.java b/core/src/main/java/com/taobao/arthas/core/util/ObjectUtils.java new file mode 100644 index 00000000..7fcbe4ad --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/ObjectUtils.java @@ -0,0 +1,672 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by Fernflower decompiler) +// + +package com.taobao.arthas.core.util; + +import java.lang.reflect.Array; +import java.util.Arrays; + +public abstract class ObjectUtils { + private static final int INITIAL_HASH = 7; + private static final int MULTIPLIER = 31; + private static final String EMPTY_STRING = ""; + private static final String NULL_STRING = "null"; + private static final String ARRAY_START = "{"; + private static final String ARRAY_END = "}"; + private static final String EMPTY_ARRAY = "{}"; + private static final String ARRAY_ELEMENT_SEPARATOR = ", "; + + public ObjectUtils() { + } + + public static boolean isCheckedException(Throwable ex) { + return !(ex instanceof RuntimeException) && !(ex instanceof Error); + } + + public static boolean isCompatibleWithThrowsClause(Throwable ex, Class... declaredExceptions) { + if(!isCheckedException(ex)) { + return true; + } else { + if(declaredExceptions != null) { + Class[] var2 = declaredExceptions; + int var3 = declaredExceptions.length; + + for(int var4 = 0; var4 < var3; ++var4) { + Class declaredException = var2[var4]; + if(declaredException.isInstance(ex)) { + return true; + } + } + } + + return false; + } + } + + public static boolean isArray(Object obj) { + return obj != null && obj.getClass().isArray(); + } + + public static boolean isEmpty(Object[] array) { + return array == null || array.length == 0; + } + + public static boolean containsElement(Object[] array, Object element) { + if(array == null) { + return false; + } else { + Object[] var2 = array; + int var3 = array.length; + + for(int var4 = 0; var4 < var3; ++var4) { + Object arrayEle = var2[var4]; + if(nullSafeEquals(arrayEle, element)) { + return true; + } + } + + return false; + } + } + + public static boolean containsConstant(Enum[] enumValues, String constant) { + return containsConstant(enumValues, constant, false); + } + + public static boolean containsConstant(Enum[] enumValues, String constant, boolean caseSensitive) { + Enum[] var3 = enumValues; + int var4 = enumValues.length; + int var5 = 0; + + while(true) { + if(var5 >= var4) { + return false; + } + + Enum candidate = var3[var5]; + if(caseSensitive) { + if(candidate.toString().equals(constant)) { + break; + } + } else if(candidate.toString().equalsIgnoreCase(constant)) { + break; + } + + ++var5; + } + + return true; + } + + public static Object[] toObjectArray(Object source) { + if(source instanceof Object[]) { + return (Object[])((Object[])source); + } else if(source == null) { + return new Object[0]; + } else if(!source.getClass().isArray()) { + throw new IllegalArgumentException("Source is not an array: " + source); + } else { + int length = Array.getLength(source); + if(length == 0) { + return new Object[0]; + } else { + Class wrapperType = Array.get(source, 0).getClass(); + Object[] newArray = (Object[])((Object[])Array.newInstance(wrapperType, length)); + + for(int i = 0; i < length; ++i) { + newArray[i] = Array.get(source, i); + } + + return newArray; + } + } + } + + public static boolean nullSafeEquals(Object o1, Object o2) { + if(o1 == o2) { + return true; + } else if(o1 != null && o2 != null) { + if(o1.equals(o2)) { + return true; + } else { + if(o1.getClass().isArray() && o2.getClass().isArray()) { + if(o1 instanceof Object[] && o2 instanceof Object[]) { + return Arrays.equals((Object[])((Object[])o1), (Object[])((Object[])o2)); + } + + if(o1 instanceof boolean[] && o2 instanceof boolean[]) { + return Arrays.equals((boolean[])((boolean[])o1), (boolean[])((boolean[])o2)); + } + + if(o1 instanceof byte[] && o2 instanceof byte[]) { + return Arrays.equals((byte[])((byte[])o1), (byte[])((byte[])o2)); + } + + if(o1 instanceof char[] && o2 instanceof char[]) { + return Arrays.equals((char[])((char[])o1), (char[])((char[])o2)); + } + + if(o1 instanceof double[] && o2 instanceof double[]) { + return Arrays.equals((double[])((double[])o1), (double[])((double[])o2)); + } + + if(o1 instanceof float[] && o2 instanceof float[]) { + return Arrays.equals((float[])((float[])o1), (float[])((float[])o2)); + } + + if(o1 instanceof int[] && o2 instanceof int[]) { + return Arrays.equals((int[])((int[])o1), (int[])((int[])o2)); + } + + if(o1 instanceof long[] && o2 instanceof long[]) { + return Arrays.equals((long[])((long[])o1), (long[])((long[])o2)); + } + + if(o1 instanceof short[] && o2 instanceof short[]) { + return Arrays.equals((short[])((short[])o1), (short[])((short[])o2)); + } + } + + return false; + } + } else { + return false; + } + } + + public static int nullSafeHashCode(Object obj) { + if(obj == null) { + return 0; + } else { + if(obj.getClass().isArray()) { + if(obj instanceof Object[]) { + return nullSafeHashCode((Object[])((Object[])obj)); + } + + if(obj instanceof boolean[]) { + return nullSafeHashCode((boolean[])((boolean[])obj)); + } + + if(obj instanceof byte[]) { + return nullSafeHashCode((byte[])((byte[])obj)); + } + + if(obj instanceof char[]) { + return nullSafeHashCode((char[])((char[])obj)); + } + + if(obj instanceof double[]) { + return nullSafeHashCode((double[])((double[])obj)); + } + + if(obj instanceof float[]) { + return nullSafeHashCode((float[])((float[])obj)); + } + + if(obj instanceof int[]) { + return nullSafeHashCode((int[])((int[])obj)); + } + + if(obj instanceof long[]) { + return nullSafeHashCode((long[])((long[])obj)); + } + + if(obj instanceof short[]) { + return nullSafeHashCode((short[])((short[])obj)); + } + } + + return obj.hashCode(); + } + } + + public static int nullSafeHashCode(Object[] array) { + if(array == null) { + return 0; + } else { + int hash = 7; + Object[] var2 = array; + int var3 = array.length; + + for(int var4 = 0; var4 < var3; ++var4) { + Object element = var2[var4]; + hash = 31 * hash + nullSafeHashCode(element); + } + + return hash; + } + } + + public static int nullSafeHashCode(boolean[] array) { + if(array == null) { + return 0; + } else { + int hash = 7; + boolean[] var2 = array; + int var3 = array.length; + + for(int var4 = 0; var4 < var3; ++var4) { + boolean element = var2[var4]; + hash = 31 * hash + hashCode(element); + } + + return hash; + } + } + + public static int nullSafeHashCode(byte[] array) { + if(array == null) { + return 0; + } else { + int hash = 7; + byte[] var2 = array; + int var3 = array.length; + + for(int var4 = 0; var4 < var3; ++var4) { + byte element = var2[var4]; + hash = 31 * hash + element; + } + + return hash; + } + } + + public static int nullSafeHashCode(char[] array) { + if(array == null) { + return 0; + } else { + int hash = 7; + char[] var2 = array; + int var3 = array.length; + + for(int var4 = 0; var4 < var3; ++var4) { + char element = var2[var4]; + hash = 31 * hash + element; + } + + return hash; + } + } + + public static int nullSafeHashCode(double[] array) { + if(array == null) { + return 0; + } else { + int hash = 7; + double[] var2 = array; + int var3 = array.length; + + for(int var4 = 0; var4 < var3; ++var4) { + double element = var2[var4]; + hash = 31 * hash + hashCode(element); + } + + return hash; + } + } + + public static int nullSafeHashCode(float[] array) { + if(array == null) { + return 0; + } else { + int hash = 7; + float[] var2 = array; + int var3 = array.length; + + for(int var4 = 0; var4 < var3; ++var4) { + float element = var2[var4]; + hash = 31 * hash + hashCode(element); + } + + return hash; + } + } + + public static int nullSafeHashCode(int[] array) { + if(array == null) { + return 0; + } else { + int hash = 7; + int[] var2 = array; + int var3 = array.length; + + for(int var4 = 0; var4 < var3; ++var4) { + int element = var2[var4]; + hash = 31 * hash + element; + } + + return hash; + } + } + + public static int nullSafeHashCode(long[] array) { + if(array == null) { + return 0; + } else { + int hash = 7; + long[] var2 = array; + int var3 = array.length; + + for(int var4 = 0; var4 < var3; ++var4) { + long element = var2[var4]; + hash = 31 * hash + hashCode(element); + } + + return hash; + } + } + + public static int nullSafeHashCode(short[] array) { + if(array == null) { + return 0; + } else { + int hash = 7; + short[] var2 = array; + int var3 = array.length; + + for(int var4 = 0; var4 < var3; ++var4) { + short element = var2[var4]; + hash = 31 * hash + element; + } + + return hash; + } + } + + public static int hashCode(boolean bool) { + return bool?1231:1237; + } + + public static int hashCode(double dbl) { + return hashCode(Double.doubleToLongBits(dbl)); + } + + public static int hashCode(float flt) { + return Float.floatToIntBits(flt); + } + + public static int hashCode(long lng) { + return (int)(lng ^ lng >>> 32); + } + + public static String identityToString(Object obj) { + return obj == null?"":obj.getClass().getName() + "@" + getIdentityHexString(obj); + } + + public static String getIdentityHexString(Object obj) { + return Integer.toHexString(System.identityHashCode(obj)); + } + + public static String getDisplayString(Object obj) { + return obj == null?"":nullSafeToString(obj); + } + + public static String nullSafeClassName(Object obj) { + return obj != null?obj.getClass().getName():"null"; + } + + public static String nullSafeToString(Object obj) { + if(obj == null) { + return "null"; + } else if(obj instanceof String) { + return (String)obj; + } else if(obj instanceof Object[]) { + return nullSafeToString((Object[])((Object[])obj)); + } else if(obj instanceof boolean[]) { + return nullSafeToString((boolean[])((boolean[])obj)); + } else if(obj instanceof byte[]) { + return nullSafeToString((byte[])((byte[])obj)); + } else if(obj instanceof char[]) { + return nullSafeToString((char[])((char[])obj)); + } else if(obj instanceof double[]) { + return nullSafeToString((double[])((double[])obj)); + } else if(obj instanceof float[]) { + return nullSafeToString((float[])((float[])obj)); + } else if(obj instanceof int[]) { + return nullSafeToString((int[])((int[])obj)); + } else if(obj instanceof long[]) { + return nullSafeToString((long[])((long[])obj)); + } else if(obj instanceof short[]) { + return nullSafeToString((short[])((short[])obj)); + } else { + String str = obj.toString(); + return str != null?str:""; + } + } + + public static String nullSafeToString(Object[] array) { + if(array == null) { + return "null"; + } else { + int length = array.length; + if(length == 0) { + return "{}"; + } else { + StringBuilder sb = new StringBuilder(); + + for(int i = 0; i < length; ++i) { + if(i == 0) { + sb.append("{"); + } else { + sb.append(", "); + } + + sb.append(String.valueOf(array[i])); + } + + sb.append("}"); + return sb.toString(); + } + } + } + + public static String nullSafeToString(boolean[] array) { + if(array == null) { + return "null"; + } else { + int length = array.length; + if(length == 0) { + return "{}"; + } else { + StringBuilder sb = new StringBuilder(); + + for(int i = 0; i < length; ++i) { + if(i == 0) { + sb.append("{"); + } else { + sb.append(", "); + } + + sb.append(array[i]); + } + + sb.append("}"); + return sb.toString(); + } + } + } + + public static String nullSafeToString(byte[] array) { + if(array == null) { + return "null"; + } else { + int length = array.length; + if(length == 0) { + return "{}"; + } else { + StringBuilder sb = new StringBuilder(); + + for(int i = 0; i < length; ++i) { + if(i == 0) { + sb.append("{"); + } else { + sb.append(", "); + } + + sb.append(array[i]); + } + + sb.append("}"); + return sb.toString(); + } + } + } + + public static String nullSafeToString(char[] array) { + if(array == null) { + return "null"; + } else { + int length = array.length; + if(length == 0) { + return "{}"; + } else { + StringBuilder sb = new StringBuilder(); + + for(int i = 0; i < length; ++i) { + if(i == 0) { + sb.append("{"); + } else { + sb.append(", "); + } + + sb.append("\'").append(array[i]).append("\'"); + } + + sb.append("}"); + return sb.toString(); + } + } + } + + public static String nullSafeToString(double[] array) { + if(array == null) { + return "null"; + } else { + int length = array.length; + if(length == 0) { + return "{}"; + } else { + StringBuilder sb = new StringBuilder(); + + for(int i = 0; i < length; ++i) { + if(i == 0) { + sb.append("{"); + } else { + sb.append(", "); + } + + sb.append(array[i]); + } + + sb.append("}"); + return sb.toString(); + } + } + } + + public static String nullSafeToString(float[] array) { + if(array == null) { + return "null"; + } else { + int length = array.length; + if(length == 0) { + return "{}"; + } else { + StringBuilder sb = new StringBuilder(); + + for(int i = 0; i < length; ++i) { + if(i == 0) { + sb.append("{"); + } else { + sb.append(", "); + } + + sb.append(array[i]); + } + + sb.append("}"); + return sb.toString(); + } + } + } + + public static String nullSafeToString(int[] array) { + if(array == null) { + return "null"; + } else { + int length = array.length; + if(length == 0) { + return "{}"; + } else { + StringBuilder sb = new StringBuilder(); + + for(int i = 0; i < length; ++i) { + if(i == 0) { + sb.append("{"); + } else { + sb.append(", "); + } + + sb.append(array[i]); + } + + sb.append("}"); + return sb.toString(); + } + } + } + + public static String nullSafeToString(long[] array) { + if(array == null) { + return "null"; + } else { + int length = array.length; + if(length == 0) { + return "{}"; + } else { + StringBuilder sb = new StringBuilder(); + + for(int i = 0; i < length; ++i) { + if(i == 0) { + sb.append("{"); + } else { + sb.append(", "); + } + + sb.append(array[i]); + } + + sb.append("}"); + return sb.toString(); + } + } + } + + public static String nullSafeToString(short[] array) { + if(array == null) { + return "null"; + } else { + int length = array.length; + if(length == 0) { + return "{}"; + } else { + StringBuilder sb = new StringBuilder(); + + for(int i = 0; i < length; ++i) { + if(i == 0) { + sb.append("{"); + } else { + sb.append(", "); + } + + sb.append(array[i]); + } + + sb.append("}"); + return sb.toString(); + } + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/SearchUtils.java b/core/src/main/java/com/taobao/arthas/core/util/SearchUtils.java new file mode 100644 index 00000000..57681cde --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/SearchUtils.java @@ -0,0 +1,136 @@ +package com.taobao.arthas.core.util; + +import com.taobao.arthas.core.GlobalOptions; +import com.taobao.arthas.core.util.matcher.Matcher; +import com.taobao.arthas.core.util.matcher.RegexMatcher; +import com.taobao.arthas.core.util.matcher.WildcardMatcher; + +import java.lang.instrument.Instrumentation; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * 类搜索工具 + * Created by vlinux on 15/5/17. + * @author diecui1202 on 2017/09/07. + */ +public class SearchUtils { + + /** + * 根据类名匹配,搜已经被JVM加载的类 + * + * @param inst inst + * @param classNameMatcher 类名匹配 + * @return 匹配的类集合 + */ + public static Set> searchClass(Instrumentation inst, Matcher classNameMatcher, int limit) { + if (classNameMatcher == null) { + return Collections.emptySet(); + } + final Set> matches = new HashSet>(); + for (Class clazz : inst.getAllLoadedClasses()) { + if (classNameMatcher.matching(clazz.getName())) { + matches.add(clazz); + } + if (matches.size() >= limit) { + break; + } + } + return matches; + } + + public static Set> searchClass(Instrumentation inst, Matcher classNameMatcher) { + return searchClass(inst, classNameMatcher, Integer.MAX_VALUE); + } + + public static Set> searchClass(Instrumentation inst, String classPattern, boolean isRegEx) { + Matcher classNameMatcher = classNameMatcher(classPattern, isRegEx); + return GlobalOptions.isDisableSubClass ? searchClass(inst, classNameMatcher) : + searchSubClass(inst, searchClass(inst, classNameMatcher)); + } + + public static Set> searchClass(Instrumentation inst, String classPattern, boolean isRegEx, String code) { + Set> matchedClasses = searchClass(inst, classPattern, isRegEx); + return filter(matchedClasses, code); + } + + public static Set> searchClassOnly(Instrumentation inst, String classPattern, boolean isRegEx) { + Matcher classNameMatcher = classNameMatcher(classPattern, isRegEx); + return searchClass(inst, classNameMatcher); + } + + public static Set> searchClassOnly(Instrumentation inst, String classPattern, int limit) { + Matcher classNameMatcher = classNameMatcher(classPattern, false); + return searchClass(inst, classNameMatcher, limit); + } + + public static Set> searchClassOnly(Instrumentation inst, String classPattern, boolean isRegEx, String code) { + Set> matchedClasses = searchClassOnly(inst, classPattern, isRegEx); + return filter(matchedClasses, code); + } + + private static Set> filter(Set> matchedClasses, String code) { + if (code == null) { + return matchedClasses; + } + + Set> result = new HashSet>(); + if (matchedClasses != null) { + for (Class c : matchedClasses) { + if (Integer.toHexString(c.getClassLoader().hashCode()).equals(code)) { + result.add(c); + } + } + } + return result; + } + + public static Matcher classNameMatcher(String classPattern, boolean isRegEx) { + if (StringUtils.isEmpty(classPattern)) { + classPattern = isRegEx ? ".*" : "*"; + } + classPattern = StringUtils.replace(classPattern, "/", "."); + return isRegEx ? new RegexMatcher(classPattern) : new WildcardMatcher(classPattern); + } + + /** + * 搜索目标类的子类 + * + * @param inst inst + * @param classSet 当前类集合 + * @return 匹配的子类集合 + */ + public static Set> searchSubClass(Instrumentation inst, Set> classSet) { + final Set> matches = new HashSet>(); + for (Class clazz : inst.getAllLoadedClasses()) { + for (Class superClass : classSet) { + if (superClass.isAssignableFrom(clazz)) { + matches.add(clazz); + break; + } + } + } + return matches; + } + + + /** + * 搜索目标类的内部类 + * + * @param inst inst + * @param c 当前类 + * @return 匹配的类的集合 + */ + public static Set> searchInnerClass(Instrumentation inst, Class c) { + final Set> matches = new HashSet>(); + for (Class clazz : inst.getInitiatedClasses(c.getClassLoader())) { + if (c.getClassLoader() != null && clazz.getClassLoader() != null && c.getClassLoader().equals(clazz.getClassLoader())) { + if (clazz.getName().startsWith(c.getName())) { + matches.add(clazz); + } + } + } + return matches; + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/StringUtils.java b/core/src/main/java/com/taobao/arthas/core/util/StringUtils.java new file mode 100644 index 00000000..0e9688d0 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/StringUtils.java @@ -0,0 +1,875 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by Fernflower decompiler) +// + +package com.taobao.arthas.core.util; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Properties; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.TreeSet; + +public abstract class StringUtils { + + private static final String FOLDER_SEPARATOR = "/"; + private static final String WINDOWS_FOLDER_SEPARATOR = "\\"; + private static final String TOP_PATH = ".."; + private static final String CURRENT_PATH = "."; + private static final char EXTENSION_SEPARATOR = '.'; + + + /** + * 获取异常的原因描述 + * + * @param t 异常 + * @return 异常原因 + */ + public static String cause(Throwable t) { + if (null != t.getCause()) { + return cause(t.getCause()); + } + return t.getMessage(); + } + + /** + * 将一个对象转换为字符串 + * + * @param obj 目标对象 + * @return 字符串 + */ + public static String objectToString(Object obj) { + if (null == obj) { + return Constants.EMPTY_STRING; + } + try { + return obj.toString(); + } catch (Throwable t) { + return "ERROR DATA!!!"; + } + } + + /** + * 翻译类名称 + * + * @param clazz Java类 + * @return 翻译值 + */ + public static String classname(Class clazz) { + if (clazz.isArray()) { + StringBuilder sb = new StringBuilder(clazz.getName()); + sb.delete(0, 2); + if (sb.length() > 0 && sb.charAt(sb.length() - 1) == ';') { + sb.deleteCharAt(sb.length() - 1); + } + sb.append("[]"); + return sb.toString(); + } else { + return clazz.getName(); + } + } + + /** + * 翻译类名称
+ * 将 java/lang/String 的名称翻译成 java.lang.String + * + * @param className 类名称 java/lang/String + * @return 翻译后名称 java.lang.String + */ + public static String normalizeClassName(String className) { + return StringUtils.replace(className, "/", "."); + } + + public static String concat(String seperator, Class... types) { + if (types == null || types.length == 0) { + return Constants.EMPTY_STRING; + } + + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < types.length; i++) { + builder.append(classname(types[i])); + if (i < types.length - 1) { + builder.append(seperator); + } + } + + return builder.toString(); + } + + /** + * 翻译Modifier值 + * + * @param mod modifier + * @return 翻译值 + */ + public static String modifier(int mod, char splitter) { + StringBuilder sb = new StringBuilder(); + if (Modifier.isAbstract(mod)) { + sb.append("abstract").append(splitter); + } + if (Modifier.isFinal(mod)) { + sb.append("final").append(splitter); + } + if (Modifier.isInterface(mod)) { + sb.append("interface").append(splitter); + } + if (Modifier.isNative(mod)) { + sb.append("native").append(splitter); + } + if (Modifier.isPrivate(mod)) { + sb.append("private").append(splitter); + } + if (Modifier.isProtected(mod)) { + sb.append("protected").append(splitter); + } + if (Modifier.isPublic(mod)) { + sb.append("public").append(splitter); + } + if (Modifier.isStatic(mod)) { + sb.append("static").append(splitter); + } + if (Modifier.isStrict(mod)) { + sb.append("strict").append(splitter); + } + if (Modifier.isSynchronized(mod)) { + sb.append("synchronized").append(splitter); + } + if (Modifier.isTransient(mod)) { + sb.append("transient").append(splitter); + } + if (Modifier.isVolatile(mod)) { + sb.append("volatile").append(splitter); + } + if (sb.length() > 0) { + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } + + /** + * 自动换行 + * + * @param string 字符串 + * @param width 行宽 + * @return 换行后的字符串 + */ + public static String wrap(String string, int width) { + final StringBuilder sb = new StringBuilder(); + final char[] buffer = string.toCharArray(); + int count = 0; + for (char c : buffer) { + + if (count == width) { + count = 0; + sb.append('\n'); + if (c == '\n') { + continue; + } + } + + if (c == '\n') { + count = 0; + } else { + count++; + } + + sb.append(c); + } + return sb.toString(); + } + + /** + *

The maximum size to which the padding constant(s) can expand.

+ */ + private static final int PAD_LIMIT = 8192; + + /** + * Represents a failed index search. + * @since 2.1 + */ + public static final int INDEX_NOT_FOUND = -1; + + public StringUtils() { + } + + public static boolean isEmpty(Object str) { + return str == null || "".equals(str); + } + + public static boolean hasLength(CharSequence str) { + return str != null && str.length() > 0; + } + + public static boolean hasLength(String str) { + return hasLength((CharSequence)str); + } + + public static boolean hasText(CharSequence str) { + if(!hasLength(str)) { + return false; + } else { + int strLen = str.length(); + + for(int i = 0; i < strLen; ++i) { + if(!Character.isWhitespace(str.charAt(i))) { + return true; + } + } + + return false; + } + } + + public static boolean hasText(String str) { + return hasText((CharSequence)str); + } + + public static boolean containsWhitespace(CharSequence str) { + if(!hasLength(str)) { + return false; + } else { + int strLen = str.length(); + + for(int i = 0; i < strLen; ++i) { + if(Character.isWhitespace(str.charAt(i))) { + return true; + } + } + + return false; + } + } + + public static boolean containsWhitespace(String str) { + return containsWhitespace((CharSequence)str); + } + + public static String trimWhitespace(String str) { + if(!hasLength(str)) { + return str; + } else { + StringBuilder sb = new StringBuilder(str); + + while(sb.length() > 0 && Character.isWhitespace(sb.charAt(0))) { + sb.deleteCharAt(0); + } + + while(sb.length() > 0 && Character.isWhitespace(sb.charAt(sb.length() - 1))) { + sb.deleteCharAt(sb.length() - 1); + } + + return sb.toString(); + } + } + + public static String trimAllWhitespace(String str) { + if(!hasLength(str)) { + return str; + } else { + StringBuilder sb = new StringBuilder(str); + int index = 0; + + while(sb.length() > index) { + if(Character.isWhitespace(sb.charAt(index))) { + sb.deleteCharAt(index); + } else { + ++index; + } + } + + return sb.toString(); + } + } + + public static String trimLeadingWhitespace(String str) { + if(!hasLength(str)) { + return str; + } else { + StringBuilder sb = new StringBuilder(str); + + while(sb.length() > 0 && Character.isWhitespace(sb.charAt(0))) { + sb.deleteCharAt(0); + } + + return sb.toString(); + } + } + + public static String trimTrailingWhitespace(String str) { + if(!hasLength(str)) { + return str; + } else { + StringBuilder sb = new StringBuilder(str); + + while(sb.length() > 0 && Character.isWhitespace(sb.charAt(sb.length() - 1))) { + sb.deleteCharAt(sb.length() - 1); + } + + return sb.toString(); + } + } + + public static String trimLeadingCharacter(String str, char leadingCharacter) { + if(!hasLength(str)) { + return str; + } else { + StringBuilder sb = new StringBuilder(str); + + while(sb.length() > 0 && sb.charAt(0) == leadingCharacter) { + sb.deleteCharAt(0); + } + + return sb.toString(); + } + } + + public static String trimTrailingCharacter(String str, char trailingCharacter) { + if(!hasLength(str)) { + return str; + } else { + StringBuilder sb = new StringBuilder(str); + + while(sb.length() > 0 && sb.charAt(sb.length() - 1) == trailingCharacter) { + sb.deleteCharAt(sb.length() - 1); + } + + return sb.toString(); + } + } + + public static boolean startsWithIgnoreCase(String str, String prefix) { + if(str != null && prefix != null) { + if(str.startsWith(prefix)) { + return true; + } else if(str.length() < prefix.length()) { + return false; + } else { + String lcStr = str.substring(0, prefix.length()).toLowerCase(); + String lcPrefix = prefix.toLowerCase(); + return lcStr.equals(lcPrefix); + } + } else { + return false; + } + } + + public static boolean endsWithIgnoreCase(String str, String suffix) { + if(str != null && suffix != null) { + if(str.endsWith(suffix)) { + return true; + } else if(str.length() < suffix.length()) { + return false; + } else { + String lcStr = str.substring(str.length() - suffix.length()).toLowerCase(); + String lcSuffix = suffix.toLowerCase(); + return lcStr.equals(lcSuffix); + } + } else { + return false; + } + } + + public static boolean substringMatch(CharSequence str, int index, CharSequence substring) { + for(int j = 0; j < substring.length(); ++j) { + int i = index + j; + if(i >= str.length() || str.charAt(i) != substring.charAt(j)) { + return false; + } + } + + return true; + } + + public static String substringAfter(String str, String separator) { + if (isEmpty(str)) { + return str; + } else if (separator == null) { + return ""; + } else { + int pos = str.indexOf(separator); + return pos == -1 ? "" : str.substring(pos + separator.length()); + } + } + + public static String substringBeforeLast(String str, String separator) { + if (!isEmpty(str) && !isEmpty(separator)) { + int pos = str.lastIndexOf(separator); + return pos == -1 ? str : str.substring(0, pos); + } else { + return str; + } + } + + public static String substringBefore(final String str, final String separator) { + if (isEmpty(str) || separator == null) { + return str; + } + if (separator.isEmpty()) { + return Constants.EMPTY_STRING; + } + final int pos = str.indexOf(separator); + if (pos == -1) { + return str; + } + return str.substring(0, pos); + } + + public static String substringAfterLast(String str, String separator) { + if (isEmpty(str)) { + return str; + } else if (isEmpty(separator)) { + return ""; + } else { + int pos = str.lastIndexOf(separator); + return pos != -1 && pos != str.length() - separator.length() ? str.substring(pos + separator.length()) : ""; + } + } + + public static int countOccurrencesOf(String str, String sub) { + if(str != null && sub != null && str.length() != 0 && sub.length() != 0) { + int count = 0; + + int idx; + for(int pos = 0; (idx = str.indexOf(sub, pos)) != -1; pos = idx + sub.length()) { + ++count; + } + + return count; + } else { + return 0; + } + } + + public static String replace(String inString, String oldPattern, String newPattern) { + if(hasLength(inString) && hasLength(oldPattern) && newPattern != null) { + StringBuilder sb = new StringBuilder(); + int pos = 0; + int index = inString.indexOf(oldPattern); + + for(int patLen = oldPattern.length(); index >= 0; index = inString.indexOf(oldPattern, pos)) { + sb.append(inString.substring(pos, index)); + sb.append(newPattern); + pos = index + patLen; + } + + sb.append(inString.substring(pos)); + return sb.toString(); + } else { + return inString; + } + } + + public static String delete(String inString, String pattern) { + return replace(inString, pattern, ""); + } + + public static String deleteAny(String inString, String charsToDelete) { + if(hasLength(inString) && hasLength(charsToDelete)) { + StringBuilder sb = new StringBuilder(); + + for(int i = 0; i < inString.length(); ++i) { + char c = inString.charAt(i); + if(charsToDelete.indexOf(c) == -1) { + sb.append(c); + } + } + + return sb.toString(); + } else { + return inString; + } + } + + public static String quote(String str) { + return str != null?"\'" + str + "\'":null; + } + + public static Object quoteIfString(Object obj) { + return obj instanceof String?quote((String)obj):obj; + } + + public static String unqualify(String qualifiedName) { + return unqualify(qualifiedName, '.'); + } + + public static String unqualify(String qualifiedName, char separator) { + return qualifiedName.substring(qualifiedName.lastIndexOf(separator) + 1); + } + + public static String capitalize(String str) { + return changeFirstCharacterCase(str, true); + } + + public static String uncapitalize(String str) { + return changeFirstCharacterCase(str, false); + } + + private static String changeFirstCharacterCase(String str, boolean capitalize) { + if(str != null && str.length() != 0) { + StringBuilder sb = new StringBuilder(str.length()); + if(capitalize) { + sb.append(Character.toUpperCase(str.charAt(0))); + } else { + sb.append(Character.toLowerCase(str.charAt(0))); + } + + sb.append(str.substring(1)); + return sb.toString(); + } else { + return str; + } + } + + + private static void validateLocalePart(String localePart) { + for(int i = 0; i < localePart.length(); ++i) { + char ch = localePart.charAt(i); + if(ch != 95 && ch != 32 && !Character.isLetterOrDigit(ch)) { + throw new IllegalArgumentException("Locale part \"" + localePart + "\" contains invalid characters"); + } + } + + } + + + public static String[] toStringArray(Collection collection) { + return collection == null?null:(String[])collection.toArray(new String[collection.size()]); + } + + public static String[] split(String toSplit, String delimiter) { + if(hasLength(toSplit) && hasLength(delimiter)) { + int offset = toSplit.indexOf(delimiter); + if(offset < 0) { + return null; + } else { + String beforeDelimiter = toSplit.substring(0, offset); + String afterDelimiter = toSplit.substring(offset + delimiter.length()); + return new String[]{beforeDelimiter, afterDelimiter}; + } + } else { + return null; + } + } + + public static Properties splitArrayElementsIntoProperties(String[] array, String delimiter) { + return splitArrayElementsIntoProperties(array, delimiter, (String)null); + } + + public static Properties splitArrayElementsIntoProperties(String[] array, String delimiter, String charsToDelete) { + if(ObjectUtils.isEmpty(array)) { + return null; + } else { + Properties result = new Properties(); + String[] var4 = array; + int var5 = array.length; + + for(int var6 = 0; var6 < var5; ++var6) { + String element = var4[var6]; + if(charsToDelete != null) { + element = deleteAny(element, charsToDelete); + } + + String[] splittedElement = split(element, delimiter); + if(splittedElement != null) { + result.setProperty(splittedElement[0].trim(), splittedElement[1].trim()); + } + } + + return result; + } + } + + public static String[] tokenizeToStringArray(String str, String delimiters) { + return tokenizeToStringArray(str, delimiters, true, true); + } + + public static String[] tokenizeToStringArray(String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) { + if(str == null) { + return null; + } else { + StringTokenizer st = new StringTokenizer(str, delimiters); + ArrayList tokens = new ArrayList(); + + while(true) { + String token; + do { + if(!st.hasMoreTokens()) { + return toStringArray((Collection)tokens); + } + + token = st.nextToken(); + if(trimTokens) { + token = token.trim(); + } + } while(ignoreEmptyTokens && token.length() <= 0); + + tokens.add(token); + } + } + } + + public static String[] delimitedListToStringArray(String str, String delimiter) { + return delimitedListToStringArray(str, delimiter, (String)null); + } + + public static String[] delimitedListToStringArray(String str, String delimiter, String charsToDelete) { + if(str == null) { + return new String[0]; + } else if(delimiter == null) { + return new String[]{str}; + } else { + ArrayList result = new ArrayList(); + int pos; + if("".equals(delimiter)) { + for(pos = 0; pos < str.length(); ++pos) { + result.add(deleteAny(str.substring(pos, pos + 1), charsToDelete)); + } + } else { + int delPos; + for(pos = 0; (delPos = str.indexOf(delimiter, pos)) != -1; pos = delPos + delimiter.length()) { + result.add(deleteAny(str.substring(pos, delPos), charsToDelete)); + } + + if(str.length() > 0 && pos <= str.length()) { + result.add(deleteAny(str.substring(pos), charsToDelete)); + } + } + + return toStringArray((Collection)result); + } + } + + public static String[] commaDelimitedListToStringArray(String str) { + return delimitedListToStringArray(str, ","); + } + + public static Set commaDelimitedListToSet(String str) { + TreeSet set = new TreeSet(); + String[] tokens = commaDelimitedListToStringArray(str); + String[] var3 = tokens; + int var4 = tokens.length; + + for(int var5 = 0; var5 < var4; ++var5) { + String token = var3[var5]; + set.add(token); + } + + return set; + } + + /** + *

+ * Joins the elements of the provided array into a single String containing the provided list of + * elements. + *

+ * + *

+ * No delimiter is added before or after the list. A null separator is the same as a + * blank String. + *

+ * + * @param array the array of values to join together + * @param separator the separator character to use + * @return the joined String + */ + public static String join(Object[] array, String separator) { + if (separator == null) { + separator = ""; + } + int arraySize = array.length; + int bufSize = (arraySize == 0 ? 0 : (array[0].toString().length() + separator.length()) * arraySize); + StringBuffer buf = new StringBuffer(bufSize); + + for (int i = 0; i < arraySize; i++) { + if (i > 0) { + buf.append(separator); + } + buf.append(array[i]); + } + return buf.toString(); + } + + /** + *

Checks if a CharSequence is whitespace, empty ("") or null.

+ * + *
+     * StringUtils.isBlank(null)      = true
+     * StringUtils.isBlank("")        = true
+     * StringUtils.isBlank(" ")       = true
+     * StringUtils.isBlank("bob")     = false
+     * StringUtils.isBlank("  bob  ") = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if the CharSequence is null, empty or whitespace + * @since 2.0 + * @since 3.0 Changed signature from isBlank(String) to isBlank(CharSequence) + */ + public static boolean isBlank(final CharSequence cs) { + int strLen; + if (cs == null || (strLen = cs.length()) == 0) { + return true; + } + for (int i = 0; i < strLen; i++) { + if (Character.isWhitespace(cs.charAt(i)) == false) { + return false; + } + } + return true; + } + + /** + *

Returns padding using the specified delimiter repeated + * to a given length.

+ * + *
+     * StringUtils.repeat('e', 0)  = ""
+     * StringUtils.repeat('e', 3)  = "eee"
+     * StringUtils.repeat('e', -2) = ""
+     * 
+ * + *

Note: this method doesn't not support padding with + * Unicode Supplementary Characters + * as they require a pair of {@code char}s to be represented. + * If you are needing to support full I18N of your applications + * consider using {@link #repeat(String, int)} instead. + *

+ * + * @param ch character to repeat + * @param repeat number of times to repeat char, negative treated as zero + * @return String with repeated character + * @see #repeat(String, int) + */ + public static String repeat(final char ch, final int repeat) { + final char[] buf = new char[repeat]; + for (int i = repeat - 1; i >= 0; i--) { + buf[i] = ch; + } + return new String(buf); + } + + /** + *

Repeat a String {@code repeat} times to form a + * new String.

+ * + *
+     * StringUtils.repeat(null, 2) = null
+     * StringUtils.repeat("", 0)   = ""
+     * StringUtils.repeat("", 2)   = ""
+     * StringUtils.repeat("a", 3)  = "aaa"
+     * StringUtils.repeat("ab", 2) = "abab"
+     * StringUtils.repeat("a", -2) = ""
+     * 
+ * + * @param str the String to repeat, may be null + * @param repeat number of times to repeat str, negative treated as zero + * @return a new String consisting of the original String repeated, + * {@code null} if null String input + */ + public static String repeat(final String str, final int repeat) { + // Performance tuned for 2.0 (JDK1.4) + + if (str == null) { + return null; + } + if (repeat <= 0) { + return Constants.EMPTY_STRING; + } + final int inputLength = str.length(); + if (repeat == 1 || inputLength == 0) { + return str; + } + if (inputLength == 1 && repeat <= PAD_LIMIT) { + return repeat(str.charAt(0), repeat); + } + + final int outputLength = inputLength * repeat; + switch (inputLength) { + case 1 : + return repeat(str.charAt(0), repeat); + case 2 : + final char ch0 = str.charAt(0); + final char ch1 = str.charAt(1); + final char[] output2 = new char[outputLength]; + for (int i = repeat * 2 - 2; i >= 0; i--, i--) { + output2[i] = ch0; + output2[i + 1] = ch1; + } + return new String(output2); + default : + final StringBuilder buf = new StringBuilder(outputLength); + for (int i = 0; i < repeat; i++) { + buf.append(str); + } + return buf.toString(); + } + } + + /** + * Gets a CharSequence length or {@code 0} if the CharSequence is + * {@code null}. + * + * @param cs + * a CharSequence or {@code null} + * @return CharSequence length or {@code 0} if the CharSequence is + * {@code null}. + * @since 2.4 + * @since 3.0 Changed signature from length(String) to length(CharSequence) + */ + public static int length(final CharSequence cs) { + return cs == null ? 0 : cs.length(); + } + + /** + *

Strips any of a set of characters from the end of a String.

+ * + *

A {@code null} input String returns {@code null}. + * An empty string ("") input returns the empty string.

+ * + *

If the stripChars String is {@code null}, whitespace is + * stripped as defined by {@link Character#isWhitespace(char)}.

+ * + *
+     * StringUtils.stripEnd(null, *)          = null
+     * StringUtils.stripEnd("", *)            = ""
+     * StringUtils.stripEnd("abc", "")        = "abc"
+     * StringUtils.stripEnd("abc", null)      = "abc"
+     * StringUtils.stripEnd("  abc", null)    = "  abc"
+     * StringUtils.stripEnd("abc  ", null)    = "abc"
+     * StringUtils.stripEnd(" abc ", null)    = " abc"
+     * StringUtils.stripEnd("  abcyx", "xyz") = "  abc"
+     * StringUtils.stripEnd("120.00", ".0")   = "12"
+     * 
+ * + * @param str the String to remove characters from, may be null + * @param stripChars the set of characters to remove, null treated as whitespace + * @return the stripped String, {@code null} if null String input + */ + public static String stripEnd(final String str, final String stripChars) { + int end; + if (str == null || (end = str.length()) == 0) { + return str; + } + + if (stripChars == null) { + while (end != 0 && Character.isWhitespace(str.charAt(end - 1))) { + end--; + } + } else if (stripChars.isEmpty()) { + return str; + } else { + while (end != 0 && stripChars.indexOf(str.charAt(end - 1)) != INDEX_NOT_FOUND) { + end--; + } + } + return str.substring(0, end); + } + + public static String classLoaderHash(Class clazz) { + if (clazz == null || clazz.getClassLoader() == null) return "null"; + return Integer.toHexString(clazz.getClassLoader().hashCode()); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/ThreadLocalWatch.java b/core/src/main/java/com/taobao/arthas/core/util/ThreadLocalWatch.java new file mode 100644 index 00000000..24349bbe --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/ThreadLocalWatch.java @@ -0,0 +1,35 @@ +package com.taobao.arthas.core.util; + +/** + * 简单的调用计时器。TODO 用stack的实现更合理 + * Created by vlinux on 16/6/1. + * @author hengyunabc 2016-10-31 + */ +public class ThreadLocalWatch { + + private final ThreadLocal timestampRef = new ThreadLocal() { + @Override + protected Long initialValue() { + return System.nanoTime(); + } + }; + + public long start() { + final long timestamp = System.nanoTime(); + timestampRef.set(timestamp); + return timestamp; + } + + public long cost() { + return (System.nanoTime() - timestampRef.get()); + } + + public double costInMillis() { + return (System.nanoTime() - timestampRef.get()) / 1000000.0; + } + + public void clear() { + timestampRef.remove(); + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/taobao/arthas/core/util/ThreadUtil.java b/core/src/main/java/com/taobao/arthas/core/util/ThreadUtil.java new file mode 100644 index 00000000..3052e9ea --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/ThreadUtil.java @@ -0,0 +1,434 @@ +package com.taobao.arthas.core.util; + +import com.taobao.arthas.core.view.Ansi; + +import java.lang.management.*; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * + * @author hengyunabc 2015年12月7日 下午2:29:28 + * + */ +abstract public class ThreadUtil { + + private static final BlockingLockInfo EMPTY_INFO = new BlockingLockInfo(); + + private static ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); + + public static ThreadGroup getRoot() { + ThreadGroup group = Thread.currentThread().getThreadGroup(); + ThreadGroup parent; + while ((parent = group.getParent()) != null) { + group = parent; + } + return group; + } + + /** + * 获取所有线程Map,以线程Name-ID为key + * + * @return + */ + public static Map getThreads() { + ThreadGroup root = getRoot(); + Thread[] threads = new Thread[root.activeCount()]; + while (root.enumerate(threads, true) == threads.length) { + threads = new Thread[threads.length * 2]; + } + SortedMap map = new TreeMap(new Comparator() { + @Override + public int compare(String o1, String o2) { + return o1.compareTo(o2); + } + }); + for (Thread thread : threads) { + if (thread != null) { + map.put(thread.getName() + "-" + thread.getId(), thread); + } + } + return map; + } + + /** + * 获取所有线程List + * + * @return + */ + public static List getThreadList() { + List result = new ArrayList(); + ThreadGroup root = getRoot(); + Thread[] threads = new Thread[root.activeCount()]; + while (root.enumerate(threads, true) == threads.length) { + threads = new Thread[threads.length * 2]; + } + for (Thread thread : threads) { + if (thread != null) { + result.add(thread); + } + } + return result; + } + + /** + * get the top N busy thread + * @param sampleInterval the interval between two samples + * @param topN the number of thread + * @return a Map representing + */ + public static Map getTopNThreads(int sampleInterval, int topN) { + List threads = getThreadList(); + + // Sample CPU + Map times1 = new HashMap(); + for (Thread thread : threads) { + long cpu = threadMXBean.getThreadCpuTime(thread.getId()); + times1.put(thread.getId(), cpu); + } + + try { + // Sleep for some time + Thread.sleep(sampleInterval); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // Resample + Map times2 = new HashMap(threads.size()); + for (Thread thread : threads) { + long cpu = threadMXBean.getThreadCpuTime(thread.getId()); + times2.put(thread.getId(), cpu); + } + + // Compute delta map and total time + long total = 0; + Map deltas = new HashMap(threads.size()); + for (Long id : times2.keySet()) { + long time1 = times2.get(id); + long time2 = times1.get(id); + if (time1 == -1) { + time1 = time2; + } else if (time2 == -1) { + time2 = time1; + } + long delta = time2 - time1; + deltas.put(id, delta); + total += delta; + } + + // Compute cpu + final HashMap cpus = new HashMap(threads.size()); + for (Thread thread : threads) { + long cpu = total == 0 ? 0 : Math.round((deltas.get(thread.getId()) * 100) / total); + cpus.put(thread, cpu); + } + + // Sort by CPU time : should be a rendering hint... + Collections.sort(threads, new Comparator() { + public int compare(Thread o1, Thread o2) { + long l1 = cpus.get(o1); + long l2 = cpus.get(o2); + if (l1 < l2) { + return 1; + } else if (l1 > l2) { + return -1; + } else { + return 0; + } + } + }); + + // use LinkedHashMap to preserve insert order + Map topNThreads = new LinkedHashMap(); + + List topThreads = topN > 0 && topN <= threads.size() + ? threads.subList(0, topN) : threads; + + for (Thread thread: topThreads) { + // Compute cpu usage + topNThreads.put(thread.getId(), cpus.get(thread)); + } + + return topNThreads; + } + + + /** + * Find the thread and lock that is blocking the most other threads. + * + * Time complexity of this algorithm: O(number of thread) + * Space complexity of this algorithm: O(number of locks) + * + * @return the BlockingLockInfo object, or an empty object if not found. + */ + public static BlockingLockInfo findMostBlockingLock() { + ThreadInfo[] infos = threadMXBean.dumpAllThreads(threadMXBean.isObjectMonitorUsageSupported(), + threadMXBean.isSynchronizerUsageSupported()); + + // a map of + Map blockCountPerLock = new HashMap(); + // a map of ownerThreadPerLock = new HashMap(); + + for (ThreadInfo info: infos) { + if (info == null) { + continue; + } + + LockInfo lockInfo = info.getLockInfo(); + if (lockInfo != null) { + // the current thread is blocked waiting on some condition + if (blockCountPerLock.get(lockInfo.getIdentityHashCode()) == null) { + blockCountPerLock.put(lockInfo.getIdentityHashCode(), 0); + } + int blockedCount = blockCountPerLock.get(lockInfo.getIdentityHashCode()); + blockCountPerLock.put(lockInfo.getIdentityHashCode(), blockedCount + 1); + } + + for (MonitorInfo monitorInfo: info.getLockedMonitors()) { + // the object monitor currently held by this thread + if (ownerThreadPerLock.get(monitorInfo.getIdentityHashCode()) == null) { + ownerThreadPerLock.put(monitorInfo.getIdentityHashCode(), info); + } + } + + for (LockInfo lockedSync: info.getLockedSynchronizers()) { + // the ownable synchronizer currently held by this thread + if (ownerThreadPerLock.get(lockedSync.getIdentityHashCode()) == null) { + ownerThreadPerLock.put(lockedSync.getIdentityHashCode(), info); + } + } + } + + // find the thread that is holding the lock that blocking the largest number of threads. + int mostBlockingLock = 0; // System.identityHashCode(null) == 0 + int maxBlockingCount = 0; + for (Map.Entry entry: blockCountPerLock.entrySet()) { + if (entry.getValue() > maxBlockingCount && ownerThreadPerLock.get(entry.getKey()) != null) { + // the lock is explicitly held by anther thread. + maxBlockingCount = entry.getValue(); + mostBlockingLock = entry.getKey(); + } + } + + if (mostBlockingLock == 0) { + // nothing found + return EMPTY_INFO; + } + + BlockingLockInfo blockingLockInfo = new BlockingLockInfo(); + blockingLockInfo.threadInfo = ownerThreadPerLock.get(mostBlockingLock); + blockingLockInfo.lockIdentityHashCode = mostBlockingLock; + blockingLockInfo.blockingThreadCount = blockCountPerLock.get(mostBlockingLock); + return blockingLockInfo; + } + + + public static String getFullStacktrace(ThreadInfo threadInfo, long cpuUsage) { + return getFullStacktrace(threadInfo, cpuUsage, 0, 0); + } + + + public static String getFullStacktrace(BlockingLockInfo blockingLockInfo) { + return getFullStacktrace(blockingLockInfo.threadInfo, -1, blockingLockInfo.lockIdentityHashCode, + blockingLockInfo.blockingThreadCount); + } + + + /** + * 完全从 ThreadInfo 中 copy 过来 + * @param threadInfo the thread info object + * @param cpuUsage will be ignore if cpuUsage < 0 or cpuUsage > 100 + * @param lockIdentityHashCode 阻塞了其他线程的锁的identityHashCode + * @param blockingThreadCount 阻塞了其他线程的数量 + * @return the string representation of the thread stack + */ + public static String getFullStacktrace(ThreadInfo threadInfo, long cpuUsage, int lockIdentityHashCode, + int blockingThreadCount) { + StringBuilder sb = new StringBuilder("\"" + threadInfo.getThreadName() + "\"" + " Id=" + + threadInfo.getThreadId()); + + if (cpuUsage >= 0 && cpuUsage <= 100) { + sb.append(" cpuUsage=").append(cpuUsage).append("%"); + } + + sb.append(" ").append(threadInfo.getThreadState()); + + if (threadInfo.getLockName() != null) { + sb.append(" on ").append(threadInfo.getLockName()); + } + if (threadInfo.getLockOwnerName() != null) { + sb.append(" owned by \"").append(threadInfo.getLockOwnerName()).append("\" Id=").append(threadInfo.getLockOwnerId()); + } + if (threadInfo.isSuspended()) { + sb.append(" (suspended)"); + } + if (threadInfo.isInNative()) { + sb.append(" (in native)"); + } + sb.append('\n'); + int i = 0; + for (; i < threadInfo.getStackTrace().length; i++) { + StackTraceElement ste = threadInfo.getStackTrace()[i]; + sb.append("\tat ").append(ste.toString()); + sb.append('\n'); + if (i == 0 && threadInfo.getLockInfo() != null) { + Thread.State ts = threadInfo.getThreadState(); + switch (ts) { + case BLOCKED: + sb.append("\t- blocked on ").append(threadInfo.getLockInfo()); + sb.append('\n'); + break; + case WAITING: + sb.append("\t- waiting on ").append(threadInfo.getLockInfo()); + sb.append('\n'); + break; + case TIMED_WAITING: + sb.append("\t- waiting on ").append(threadInfo.getLockInfo()); + sb.append('\n'); + break; + default: + } + } + + for (MonitorInfo mi : threadInfo.getLockedMonitors()) { + if (mi.getLockedStackDepth() == i) { + sb.append("\t- locked ").append(mi); + if (mi.getIdentityHashCode() == lockIdentityHashCode) { + Ansi highlighted = Ansi.ansi().fg(Ansi.Color.RED); + highlighted.a(" <---- but blocks ").a(blockingThreadCount).a(" other threads!"); + sb.append(highlighted.reset().toString()); + } + sb.append('\n'); + } + } + } + if (i < threadInfo.getStackTrace().length) { + sb.append("\t..."); + sb.append('\n'); + } + + LockInfo[] locks = threadInfo.getLockedSynchronizers(); + if (locks.length > 0) { + sb.append("\n\tNumber of locked synchronizers = ").append(locks.length); + sb.append('\n'); + for (LockInfo li : locks) { + sb.append("\t- ").append(li); + if (li.getIdentityHashCode() == lockIdentityHashCode) { + sb.append(" <---- but blocks ").append(blockingThreadCount); + sb.append(" other threads!"); + } + sb.append('\n'); + } + } + sb.append('\n'); + return sb.toString().replace("\t", " "); + } + + public static class BlockingLockInfo { + + // the thread info that is holing this lock. + public ThreadInfo threadInfo = null; + // the associated LockInfo object + public int lockIdentityHashCode = 0; + // the number of thread that is blocked on this lock + public int blockingThreadCount = 0; + + } + + + /** + * 获取方法执行堆栈信息 + * + * @return 方法堆栈信息 + */ + public static String getThreadStack(Thread currentThread) { + StackTraceElement[] stackTraceElementArray = currentThread.getStackTrace(); + + StackTraceElement locationStackTraceElement = stackTraceElementArray[10]; + String locationString = String.format(" @%s.%s()", locationStackTraceElement.getClassName(), + locationStackTraceElement.getMethodName()); + + StringBuilder builder = new StringBuilder(); + builder.append(getThreadTitle(currentThread)).append("\n").append(locationString).append("\n"); + + int skip = 11; + for (int index = skip; index < stackTraceElementArray.length; index++) { + StackTraceElement ste = stackTraceElementArray[index]; + builder.append(" at ") + .append(ste.getClassName()) + .append(".") + .append(ste.getMethodName()) + .append("(") + .append(ste.getFileName()) + .append(":") + .append(ste.getLineNumber()) + .append(")\n"); + } + + return builder.toString(); + } + + public static String getThreadTitle(Thread currentThread) { + StringBuilder sb = new StringBuilder("thread_name="); + sb.append(currentThread.getName()) + .append(";id=").append(Long.toHexString(currentThread.getId())) + .append(";is_daemon=").append(currentThread.isDaemon()) + .append(";priority=").append(currentThread.getPriority()) + .append(";TCCL=").append(getTCCL(currentThread)); + getEagleeyeTraceInfo(currentThread, sb); + return sb.toString(); + } + + private static String getTCCL(Thread currentThread) { + if (null == currentThread.getContextClassLoader()) { + return "null"; + } else { + return currentThread.getContextClassLoader().getClass().getName() + + "@" + Integer.toHexString(currentThread.getContextClassLoader().hashCode()); + } + } + + private static void getEagleeyeTraceInfo(Thread currentThread, StringBuilder sb) { + try { + // access to Thread#threadlocals field + Field threadLocalsField = Thread.class.getDeclaredField("threadLocals"); + threadLocalsField.setAccessible(true); + Object threadLocalMap = threadLocalsField.get(currentThread); + // access to ThreadLocal$ThreadLocalMap#table filed + Field tableFiled = threadLocalMap.getClass().getDeclaredField("table"); + tableFiled.setAccessible(true); + Object[] tableEntries = (Object[])tableFiled.get(threadLocalMap); + for (Object entry: tableEntries) { + if (entry == null) { + continue; + } + // access to ThreadLocal$ThreadLocalMap$Entry#value field + Field valueField = entry.getClass().getDeclaredField("value"); + valueField.setAccessible(true); + Object threadLocalValue = valueField.get(entry); + if (threadLocalValue != null && + "com.taobao.eagleeye.RpcContext_inner".equals(threadLocalValue.getClass().getName())) { + // finally we got the chance to access trace id + Method getTraceIdMethod = threadLocalValue.getClass().getMethod("getTraceId"); + getTraceIdMethod.setAccessible(true); + String traceId = (String)getTraceIdMethod.invoke(threadLocalValue); + sb.append(";trace_id=").append(traceId); + // get rpc id + Method getRpcIdMethod = threadLocalValue.getClass().getMethod("getRpcId"); + getTraceIdMethod.setAccessible(true); + String rpcId = (String)getRpcIdMethod.invoke(threadLocalValue); + sb.append(";rpc_id=").append(rpcId); + return; + } + } + } catch (Exception e) { + // ignore + } + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/TokenUtils.java b/core/src/main/java/com/taobao/arthas/core/util/TokenUtils.java new file mode 100644 index 00000000..e4159e80 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/TokenUtils.java @@ -0,0 +1,48 @@ +package com.taobao.arthas.core.util; + +import java.util.List; + +import com.taobao.arthas.core.shell.cli.CliToken; + +/** + * tokens处理的辅助类 + * + * @author gehui 2017年7月27日 上午11:39:56 + */ +public class TokenUtils { + + public static CliToken findFirstTextToken(List tokens) { + CliToken first = null; + for (CliToken token : tokens) { + if (token.isText()) { + first = token; + break; + } + } + return first; + } + + public static CliToken findLastTextToken(List tokens) { + for (int i = tokens.size() - 1; i > 0; i--) { + CliToken token = tokens.get(i); + if (token.isText()) { + return token; + } + } + return null; + } + + public static String findSecondTokenText(List tokens) { + boolean first = true; + for (CliToken token : tokens) { + if (token.isText()) { + if (first) { + first = false; + } else { + return token.value(); + } + } + } + return null; + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/TypeRenderUtils.java b/core/src/main/java/com/taobao/arthas/core/util/TypeRenderUtils.java new file mode 100644 index 00000000..430ae5c5 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/TypeRenderUtils.java @@ -0,0 +1,165 @@ +package com.taobao.arthas.core.util; + +import com.taobao.arthas.core.view.ObjectView; +import com.taobao.text.ui.Element; +import com.taobao.text.ui.TableElement; +import com.taobao.text.ui.TreeElement; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import static com.taobao.text.ui.Element.label; + +/** + * @author beiwei30 on 24/11/2016. + */ +public class TypeRenderUtils { + + public static String drawAnnotation(Class clazz) { + return drawAnnotation(clazz.getDeclaredAnnotations()); + } + + public static String drawAnnotation(Method method) { + return drawAnnotation(method.getDeclaredAnnotations()); + } + + public static String drawInterface(Class clazz) { + return StringUtils.concat(",", clazz.getInterfaces()); + } + + public static String drawParameters(Method method) { + return StringUtils.concat("\n", method.getParameterTypes()); + } + + public static String drawParameters(Constructor constructor) { + return StringUtils.concat("\n", constructor.getParameterTypes()); + } + + public static String drawReturn(Method method) { + return StringUtils.classname(method.getReturnType()); + } + + public static String drawExceptions(Method method) { + return StringUtils.concat("\n", method.getExceptionTypes()); + } + + public static String drawExceptions(Constructor constructor) { + return StringUtils.concat("\n", constructor.getExceptionTypes()); + } + + public static Element drawSuperClass(Class clazz) { + TreeElement root = new TreeElement(); + TreeElement parent = root; + + Class superClass = clazz.getSuperclass(); + if (null != superClass) { + TreeElement child = new TreeElement(label(StringUtils.classname(superClass))); + parent.addChild(child); + parent = child; + + while (true) { + superClass = superClass.getSuperclass(); + if (null == superClass) { + break; + } + TreeElement tempChild = new TreeElement(label(StringUtils.classname(superClass))); + parent.addChild(tempChild); + parent = tempChild; + } + } + return root; + } + + public static Element drawClassLoader(Class clazz) { + TreeElement root = new TreeElement(); + TreeElement parent = root; + ClassLoader loader = clazz.getClassLoader(); + if (null != loader) { + TreeElement child = new TreeElement(label(loader.toString())); + parent.addChild(child); + parent = child; + while (true) { + loader = loader.getParent(); + if (null == loader) { + break; + } + TreeElement tempChild = new TreeElement(label(loader.toString())); + parent.addChild(tempChild); + parent = tempChild; + } + } + return root; + } + + public static Element drawField(Class clazz, Integer expand) { + TableElement fieldsTable = new TableElement().leftCellPadding(0).rightCellPadding(0); + Field[] fields = clazz.getDeclaredFields(); + if (fields == null || fields.length == 0) { + return fieldsTable; + } + + for (Field field : fields) { + TableElement fieldTable = new TableElement().leftCellPadding(0).rightCellPadding(0); + fieldTable.row("modifier", StringUtils.modifier(field.getModifiers(), ',')) + .row("type", StringUtils.classname(field.getType())) + .row("name", field.getName()); + + Annotation[] annotations = field.getAnnotations(); + if (annotations != null && annotations.length > 0) { + fieldTable.row("annotation", drawAnnotation(annotations)); + } + + if (Modifier.isStatic(field.getModifiers())) { + fieldTable.row("value", drawFieldValue(field, expand)); + } + + fieldTable.row(label("")); + fieldsTable.row(fieldTable); + } + + return fieldsTable; + } + + public static String renderMethodSignature(Method method) { + StringBuilder sb = new StringBuilder(); + sb.append(StringUtils.modifier(method.getModifiers(), ' ')).append(" "); + sb.append(TypeRenderUtils.drawReturn(method)).append(" "); + sb.append(method.getName()).append(" "); + sb.append("("); + sb.append(StringUtils.concat(", ", method.getParameterTypes())); + sb.append(")"); + return sb.toString(); + } + + private static String drawFieldValue(Field field, Integer expand) { + final boolean isAccessible = field.isAccessible(); + try { + field.setAccessible(true); + Object value = field.get(null); + Object o = (expand != null && expand >= 0) ? new ObjectView(value, expand).draw() : value; + return StringUtils.objectToString(o); + } catch (IllegalAccessException e) { + // no op + } finally { + field.setAccessible(isAccessible); + } + return Constants.EMPTY_STRING; + } + + public static String drawAnnotation(Annotation... annotations) { + List> types = Collections.emptyList(); + if (annotations != null && annotations.length > 0) { + types = new LinkedList>(); + for (Annotation annotation : annotations) { + types.add(annotation.annotationType()); + } + } + return StringUtils.concat(",", types.toArray(new Class[types.size()])); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/UserStatUtil.java b/core/src/main/java/com/taobao/arthas/core/util/UserStatUtil.java new file mode 100644 index 00000000..e8b2e358 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/UserStatUtil.java @@ -0,0 +1,112 @@ +package com.taobao.arthas.core.util; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLEncoder; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Arthas 使用情况统计 + *

+ * Created by zhuyong on 15/11/12. + */ +public class UserStatUtil { + private static final ExecutorService executorService = Executors.newSingleThreadExecutor(); + private static final String ip = IPUtils.getLocalIP(); + + private static final String version = URLEncoder.encode(ArthasBanner.version().replace("\n", "")); + + public static void arthasStart() { + RemoteJob job = new RemoteJob(); + job.setResource("anonymousStatStart.do"); + job.appendQueryData("productName", "Arthas"); + job.appendQueryData("productVersion", URLEncoder.encode(ArthasBanner.version())); + + try { + executorService.execute(job); + } catch(Throwable t) { + // + } + } + + public static void arthasUsage(String cmd, String detail) { + RemoteJob job = new RemoteJob(); + job.setResource("nonAnonymousStat.do"); + job.appendQueryData("ip", ip); + job.appendQueryData("productName", "Arthas"); + job.appendQueryData("productVersion", version); + job.appendQueryData("opName", URLEncoder.encode(cmd)); + if (detail != null) { + job.appendQueryData("opDetail", URLEncoder.encode(detail)); + } + + try { + executorService.execute(job); + } catch(Throwable t) { + // + } + } + + public static void arthasUsageSuccess(String cmd, List args) { + StringBuilder commandString = new StringBuilder(cmd); + for (String arg: args) { + commandString.append(" ").append(arg); + } + UserStatUtil.arthasUsage(cmd, commandString.toString() + " --> success"); + } + + public static void destroy() { + // 直接关闭,没有回报的丢弃 + executorService.shutdownNow(); + } + + static class RemoteJob implements Runnable { + + // private StringBuilder link = new StringBuilder("http://arthas.io/api/"); + private StringBuilder link = new StringBuilder("http://arthas.io/api/"); + + private String resource; + + private StringBuilder queryData = new StringBuilder(); + + public void setResource(String resource) { + this.resource = resource; + } + + public void appendQueryData(String key, String value) { + if (key != null && value != null) { + if (queryData.length() == 0) { + queryData.append(key + "=" + value); + } else { + queryData.append("&" + key + "=" + value); + } + } + } + + @Override + public void run() { + try { + link.append(resource); + if (queryData.length() != 0) { + link.append("?").append(queryData); + } + URL url = new URL(link.toString()); + URLConnection connection = url.openConnection(); + connection.setConnectTimeout(1000); + connection.setReadTimeout(1000); + connection.connect(); + BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream())); + String line = null; + StringBuilder result = new StringBuilder(); + while ((line = br.readLine()) != null) { + result.append(line); + } + } catch (Exception ex) { + } + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/affect/Affect.java b/core/src/main/java/com/taobao/arthas/core/util/affect/Affect.java new file mode 100644 index 00000000..877d8b4a --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/affect/Affect.java @@ -0,0 +1,27 @@ +package com.taobao.arthas.core.util.affect; + +import static java.lang.System.currentTimeMillis; + +/** + * 影响反馈 + * Created by vlinux on 15/5/21. + * @author diecui1202 on 2017/10/26 + */ +public class Affect { + + private final long start = currentTimeMillis(); + + /** + * 影响耗时(ms) + * + * @return 获取耗时(ms) + */ + public long cost() { + return currentTimeMillis() - start; + } + + @Override + public String toString() { + return String.format("Affect cost in %s ms.", cost()); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/affect/EnhancerAffect.java b/core/src/main/java/com/taobao/arthas/core/util/affect/EnhancerAffect.java new file mode 100644 index 00000000..131ec24d --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/affect/EnhancerAffect.java @@ -0,0 +1,100 @@ +package com.taobao.arthas.core.util.affect; + +import com.taobao.arthas.core.GlobalOptions; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.atomic.AtomicInteger; + +import static java.lang.String.format; + +/** + * 增强影响范围
+ * 统计影响类/方法/耗时 + * Created by vlinux on 15/5/19. + */ +public final class EnhancerAffect extends Affect { + + private final AtomicInteger cCnt = new AtomicInteger(); + private final AtomicInteger mCnt = new AtomicInteger(); + + /** + * dumpClass的文件存放集合 + */ + private final Collection classDumpFiles = new ArrayList(); + + public EnhancerAffect() { + + } + + public EnhancerAffect(int cCnt, int mCnt) { + this.cCnt(cCnt); + this.mCnt(mCnt); + } + + /** + * 影响类统计 + * + * @param cc 类影响计数 + * @return 当前影响类个数 + */ + public int cCnt(int cc) { + return cCnt.addAndGet(cc); + } + + /** + * 影响方法统计 + * + * @param mc 方法影响计数 + * @return 当前影响方法个数 + */ + public int mCnt(int mc) { + return mCnt.addAndGet(mc); + } + + /** + * 获取影响类个数 + * + * @return 影响类个数 + */ + public int cCnt() { + return cCnt.get(); + } + + /** + * 获取影响方法个数 + * + * @return 影响方法个数 + */ + public int mCnt() { + return mCnt.get(); + } + + /** + * 获取dump的Class文件集合 + * + * @return classDumpList + */ + public Collection getClassDumpFiles() { + return classDumpFiles; + } + + @Override + public String toString() { + final StringBuilder infoSB = new StringBuilder(); + if (GlobalOptions.isDump + && !classDumpFiles.isEmpty()) { + + for (File classDumpFile : classDumpFiles) { + infoSB.append("[dump: ").append(classDumpFile.getAbsoluteFile()).append("]\n"); + } + } + infoSB.append(format("Affect(class-cnt:%d , method-cnt:%d) cost in %s ms.", + cCnt(), + mCnt(), + cost())); + return infoSB.toString(); + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/affect/RowAffect.java b/core/src/main/java/com/taobao/arthas/core/util/affect/RowAffect.java new file mode 100644 index 00000000..f7b00f07 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/affect/RowAffect.java @@ -0,0 +1,45 @@ +package com.taobao.arthas.core.util.affect; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 行记录影响反馈 + * Created by vlinux on 15/5/21. + */ +public final class RowAffect extends Affect { + + private final AtomicInteger rCnt = new AtomicInteger(); + + public RowAffect() { + } + + public RowAffect(int rCnt) { + this.rCnt(rCnt); + } + + /** + * 影响行数统计 + * + * @param mc 行影响计数 + * @return 当前影响行个数 + */ + public int rCnt(int mc) { + return rCnt.addAndGet(mc); + } + + /** + * 获取影响行个数 + * + * @return 影响行个数 + */ + public int rCnt() { + return rCnt.get(); + } + + @Override + public String toString() { + return String.format("Affect(row-cnt:%d) cost in %s ms.", + rCnt(), + cost()); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/collection/GaStack.java b/core/src/main/java/com/taobao/arthas/core/util/collection/GaStack.java new file mode 100644 index 00000000..80e79ff5 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/collection/GaStack.java @@ -0,0 +1,18 @@ +package com.taobao.arthas.core.util.collection; + +/** + * 堆栈 + * Created by vlinux on 15/6/21. + * @param + */ +public interface GaStack { + + E pop(); + + void push(E e); + + E peek(); + + boolean isEmpty(); + +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/collection/ThreadUnsafeFixGaStack.java b/core/src/main/java/com/taobao/arthas/core/util/collection/ThreadUnsafeFixGaStack.java new file mode 100644 index 00000000..95708427 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/collection/ThreadUnsafeFixGaStack.java @@ -0,0 +1,60 @@ +package com.taobao.arthas.core.util.collection; + +import java.util.NoSuchElementException; + +/** + * 线程不安全固定栈深的堆栈实现
+ * 固定堆栈深度的实现能比JDK自带的堆栈实现提高10倍的性能. + * Created by vlinux on 15/6/21. + * @param + */ +public class ThreadUnsafeFixGaStack implements GaStack { + + private final static int EMPTY_INDEX = -1; + private final Object[] elementArray; + private final int max; + private int current = EMPTY_INDEX; + + public ThreadUnsafeFixGaStack(int max) { + this.max = max; + this.elementArray = new Object[max]; + } + + private void checkForPush() { + // stack is full + if (current == max) { + throw new ArrayIndexOutOfBoundsException(); + } + } + + private void checkForPopOrPeek() { + // stack is empty + if (isEmpty()) { + throw new NoSuchElementException(); + } + } + + @Override + public E pop() { + checkForPopOrPeek(); + return (E) elementArray[current--]; + } + + @Override + public void push(E e) { + checkForPush(); + elementArray[++current] = e; + } + + @Override + public E peek() { + checkForPopOrPeek(); + return (E) elementArray[current]; + } + + @Override + public boolean isEmpty() { + return current == EMPTY_INDEX; + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/collection/ThreadUnsafeGaStack.java b/core/src/main/java/com/taobao/arthas/core/util/collection/ThreadUnsafeGaStack.java new file mode 100644 index 00000000..c35b64fe --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/collection/ThreadUnsafeGaStack.java @@ -0,0 +1,90 @@ +package com.taobao.arthas.core.util.collection; + +import com.taobao.arthas.core.util.LogUtil; + +import java.util.NoSuchElementException; + +import static java.lang.System.arraycopy; + +/** + * 线程不安全不固定栈深的堆栈实现
+ * 比默认的实现带来3倍的性能提升 + * Created by vlinux on 15/6/21. + * + * @param + */ +public class ThreadUnsafeGaStack implements GaStack { + + private final static int EMPTY_INDEX = -1; + private final static int DEFAULT_STACK_DEEP = 12; + + private Object[] elementArray; + private int current = EMPTY_INDEX; + + public ThreadUnsafeGaStack() { + this(DEFAULT_STACK_DEEP); + } + + private ThreadUnsafeGaStack(int stackSize) { + this.elementArray = new Object[stackSize]; + } + + /** + * 自动扩容
+ * 当前堆栈最大深度不满足期望时会自动扩容(2倍扩容) + * + * @param expectDeep 期望堆栈深度 + */ + private void ensureCapacityInternal(int expectDeep) { + final int currentStackSize = elementArray.length; + if (elementArray.length <= expectDeep) { + LogUtil.getArthasLogger().debug("resize GaStack to double length: " + currentStackSize * 2 + " for thread: " + + Thread.currentThread().getName()); + final Object[] newElementArray = new Object[currentStackSize * 2]; + arraycopy(elementArray, 0, newElementArray, 0, currentStackSize); + this.elementArray = newElementArray; + } + } + + private void checkForPopOrPeek() { + // stack is empty + if (isEmpty()) { + throw new NoSuchElementException(); + } + } + + @Override + public E pop() { + try { + checkForPopOrPeek(); + E res = (E) elementArray[current]; + elementArray[current] = null; + current--; + return res; + } finally { + if (current == EMPTY_INDEX && elementArray.length > DEFAULT_STACK_DEEP) { + elementArray = new Object[DEFAULT_STACK_DEEP]; + LogUtil.getArthasLogger().debug( + "resize GaStack to default length for thread: " + Thread.currentThread().getName()); + } + } + } + + @Override + public void push(E e) { + ensureCapacityInternal(current + 1); + elementArray[++current] = e; + } + + @Override + public E peek() { + checkForPopOrPeek(); + return (E) elementArray[current]; + } + + @Override + public boolean isEmpty() { + return current == EMPTY_INDEX; + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/matcher/EqualsMatcher.java b/core/src/main/java/com/taobao/arthas/core/util/matcher/EqualsMatcher.java new file mode 100644 index 00000000..8c3ca113 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/matcher/EqualsMatcher.java @@ -0,0 +1,21 @@ +package com.taobao.arthas.core.util.matcher; + +import com.taobao.arthas.core.util.ArthasCheckUtils; + +/** + * 字符串全匹配 + * @author ralf0131 2017-01-06 13:18. + */ +public class EqualsMatcher implements Matcher { + + private final T pattern; + + public EqualsMatcher(T pattern) { + this.pattern = pattern; + } + + @Override + public boolean matching(T target) { + return ArthasCheckUtils.isEquals(target, pattern); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/matcher/FalseMatcher.java b/core/src/main/java/com/taobao/arthas/core/util/matcher/FalseMatcher.java new file mode 100644 index 00000000..4294834f --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/matcher/FalseMatcher.java @@ -0,0 +1,12 @@ +package com.taobao.arthas.core.util.matcher; + +/** + * @author ralf0131 2017-01-06 13:33. + */ +public class FalseMatcher implements Matcher { + + @Override + public boolean matching(T target) { + return false; + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/matcher/GroupMatcher.java b/core/src/main/java/com/taobao/arthas/core/util/matcher/GroupMatcher.java new file mode 100644 index 00000000..5746c379 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/matcher/GroupMatcher.java @@ -0,0 +1,97 @@ +package com.taobao.arthas.core.util.matcher; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; + +/** + * @author ralf0131 2017-01-06 13:29. + */ +public interface GroupMatcher extends Matcher { + + /** + * 追加匹配器 + * + * @param matcher 匹配器 + */ + void add(Matcher matcher); + + /** + * 与关系组匹配 + * + * @param 匹配类型 + */ + class And implements GroupMatcher { + + private final Collection> matchers; + + /** + * 与关系组匹配构造
+ * 当且仅当目标符合匹配组的所有条件时才判定匹配成功 + * + * @param matchers 待进行与关系组匹配的匹配集合 + */ + public And(Matcher... matchers) { + this.matchers = Arrays.asList(matchers); + } + + @Override + public boolean matching(T target) { + for (Matcher matcher : matchers) { + if (!matcher.matching(target)) { + return false; + } + } + return true; + } + + @Override + public void add(Matcher matcher) { + matchers.add(matcher); + } + } + + /** + * 或关系组匹配 + * + * @param 匹配类型 + */ + class Or implements GroupMatcher { + + private final Collection> matchers; + + public Or() { + this.matchers = new ArrayList>(); + } + + /** + * 或关系组匹配构造
+ * 当且仅当目标符合匹配组的任一条件时就判定匹配成功 + * + * @param matchers 待进行或关系组匹配的匹配集合 + */ + public Or(Matcher... matchers) { + this.matchers = Arrays.asList(matchers); + } + + public Or(Collection> matchers) { + this.matchers = matchers; + } + + @Override + public boolean matching(T target) { + for (Matcher matcher : matchers) { + if (matcher.matching(target)) { + return true; + } + } + return false; + } + + @Override + public void add(Matcher matcher) { + matchers.add(matcher); + } + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/matcher/Matcher.java b/core/src/main/java/com/taobao/arthas/core/util/matcher/Matcher.java new file mode 100644 index 00000000..0461b4b5 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/matcher/Matcher.java @@ -0,0 +1,17 @@ +package com.taobao.arthas.core.util.matcher; + +/** + * 匹配器 + * Created by vlinux on 15/5/17. + */ +public interface Matcher { + + /** + * 是否匹配 + * + * @param target 目标字符串 + * @return 目标字符串是否匹配表达式 + */ + boolean matching(T target); + +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/matcher/RegexMatcher.java b/core/src/main/java/com/taobao/arthas/core/util/matcher/RegexMatcher.java new file mode 100644 index 00000000..4c6673dc --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/matcher/RegexMatcher.java @@ -0,0 +1,21 @@ +package com.taobao.arthas.core.util.matcher; + +/** + * 正则表达式匹配 + * @author ralf0131 2017-01-06 13:16. + */ +public class RegexMatcher implements Matcher { + + private final String pattern; + + public RegexMatcher(String pattern) { + this.pattern = pattern; + } + + @Override + public boolean matching(String target) { + return null != target + && null != pattern + && target.matches(pattern); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/taobao/arthas/core/util/matcher/TrueMatcher.java b/core/src/main/java/com/taobao/arthas/core/util/matcher/TrueMatcher.java new file mode 100644 index 00000000..28173b9c --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/matcher/TrueMatcher.java @@ -0,0 +1,13 @@ +package com.taobao.arthas.core.util.matcher; + +/** + * @author ralf0131 2017-01-06 13:48. + */ +public final class TrueMatcher implements Matcher { + + @Override + public boolean matching(T target) { + return true; + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/taobao/arthas/core/util/matcher/WildcardMatcher.java b/core/src/main/java/com/taobao/arthas/core/util/matcher/WildcardMatcher.java new file mode 100644 index 00000000..e46885c2 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/matcher/WildcardMatcher.java @@ -0,0 +1,98 @@ +package com.taobao.arthas.core.util.matcher; + +/** + * 通配符表达式匹配 + * @author ralf0131 2017-01-06 13:17. + */ +public class WildcardMatcher implements Matcher { + + private final String pattern; + + public WildcardMatcher(String pattern) { + this.pattern = pattern; + } + + + @Override + public boolean matching(String target) { + return match(target, pattern, 0, 0); + } + + /** + * Internal matching recursive function. + */ + private boolean match(String string, String pattern, int stringStartNdx, int patternStartNdx) { + int pNdx = patternStartNdx; + int sNdx = stringStartNdx; + int pLen = pattern.length(); + if (pLen == 1) { + if (pattern.charAt(0) == '*') { // speed-up + return true; + } + } + int sLen = string.length(); + boolean nextIsNotWildcard = false; + + while (true) { + + // check if end of string and/or pattern occurred + if ((sNdx >= sLen)) { // end of string still may have pending '*' callback pattern + while ((pNdx < pLen) && (pattern.charAt(pNdx) == '*')) { + pNdx++; + } + return pNdx >= pLen; + } + if (pNdx >= pLen) { // end of pattern, but not end of the string + return false; + } + char p = pattern.charAt(pNdx); // pattern char + + // perform logic + if (!nextIsNotWildcard) { + + if (p == '\\') { + pNdx++; + nextIsNotWildcard = true; + continue; + } + if (p == '?') { + sNdx++; + pNdx++; + continue; + } + if (p == '*') { + char pnext = 0; // next pattern char + if (pNdx + 1 < pLen) { + pnext = pattern.charAt(pNdx + 1); + } + if (pnext == '*') { // double '*' have the same effect as one '*' + pNdx++; + continue; + } + int i; + pNdx++; + + // find recursively if there is any substring from the end of the + // line that matches the rest of the pattern !!! + for (i = string.length(); i >= sNdx; i--) { + if (match(string, pattern, i, pNdx)) { + return true; + } + } + return false; + } + } else { + nextIsNotWildcard = false; + } + + // check if pattern char and string char are equals + if (p != string.charAt(sNdx)) { + return false; + } + + // everything matches for now, continue + sNdx++; + pNdx++; + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/metrics/RateCounter.java b/core/src/main/java/com/taobao/arthas/core/util/metrics/RateCounter.java new file mode 100644 index 00000000..a0ab06b8 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/metrics/RateCounter.java @@ -0,0 +1,91 @@ +package com.taobao.arthas.core.util.metrics; + +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicLongArray; + +/** + *

+ * 统计平均速率,比如统计5秒内的平均速率。
+ * 5秒的数据是:234, 345,124,366,235,
+ * 则速率是 (234+345+124+366+235)/5 = 260
+ * 
+ * 
+ * + * @author hengyunabc 2015年12月18日 下午3:40:19 + * + */ +public class RateCounter { + private static final int BITS_PER_LONG = 63; + public static final int DEFAULT_SIZE = 5; + + private final AtomicLong count = new AtomicLong(); + private final AtomicLongArray values; + + public RateCounter() { + this(DEFAULT_SIZE); + } + + public RateCounter(int size) { + this.values = new AtomicLongArray(size); + for (int i = 0; i < values.length(); i++) { + values.set(i, 0); + } + count.set(0); + } + + public int size() { + final long c = count.get(); + if (c > values.length()) { + return values.length(); + } + return (int) c; + } + + public void update(long value) { + final long c = count.incrementAndGet(); + if (c <= values.length()) { + values.set((int) c - 1, value); + } else { + final long r = nextLong(c); + if (r < values.length()) { + values.set((int) r, value); + } + } + } + + public double rate() { + long c = count.get(); + int countLength = 0; + long sum = 0; + if (c > values.length()) { + countLength = values.length(); + } else { + countLength = (int) c; + } + + for (int i = 0; i < countLength; ++i) { + sum += values.get(i); + } + + return sum / (double) countLength; + } + + /** + * Get a pseudo-random long uniformly between 0 and n-1. Stolen from + * {@link java.util.Random#nextInt()}. + * + * @param n + * the bound + * @return a value select randomly from the range {@code [0..n)}. + */ + private static long nextLong(long n) { + long bits, val; + do { + bits = ThreadLocalRandom.current().nextLong() & (~(1L << BITS_PER_LONG)); + val = bits % n; + } while (bits - val + (n - 1) < 0L); + return val; + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/metrics/SumRateCounter.java b/core/src/main/java/com/taobao/arthas/core/util/metrics/SumRateCounter.java new file mode 100644 index 00000000..42c5403f --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/metrics/SumRateCounter.java @@ -0,0 +1,48 @@ +package com.taobao.arthas.core.util.metrics; + +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicLongArray; + +/** + *
+ * 统计传入的数据是总数的速率。
+ * 比如传入的数据是所有请求的数量,5秒数据为:
+ * 267, 457, 635, 894, 1398
+ * 则统计的平均速率是:( (457-267) + (635-457) + (894-635) + (1398-894) ) / 4 = 282
+ * 
+ * + * @author hengyunabc 2015年12月18日 下午3:40:26 + * + */ +public class SumRateCounter { + + RateCounter rateCounter; + + Long previous = null; + + public SumRateCounter() { + rateCounter = new RateCounter(); + } + + public SumRateCounter(int size) { + rateCounter = new RateCounter(size); + } + + public int size() { + return rateCounter.size(); + } + + public void update(long value) { + if (previous == null) { + previous = value; + return; + } + rateCounter.update(value - previous); + previous = value; + } + + public double rate() { + return rateCounter.rate(); + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/reflect/ArthasReflectUtils.java b/core/src/main/java/com/taobao/arthas/core/util/reflect/ArthasReflectUtils.java new file mode 100644 index 00000000..1a84e5dc --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/reflect/ArthasReflectUtils.java @@ -0,0 +1,321 @@ +package com.taobao.arthas.core.util.reflect; + +import com.taobao.arthas.core.util.ArthasCheckUtils; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLDecoder; +import java.util.Collections; +import java.util.Enumeration; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * 反射工具类 Created by vlinux on 15/5/18. + */ +public class ArthasReflectUtils { + + /** + * 从包package中获取所有的Class + * + * @param packname 包名称 + * @return 包路径下所有类集合 + *

+ * 代码摘抄自 http://www.oschina.net/code/snippet_129830_8767

+ */ + public static Set> getClasses(final ClassLoader loader, final String packname) { + + // 第一个class类的集合 + Set> classes = new LinkedHashSet>(); + // 是否循环迭代 + // 获取包的名字 并进行替换 + String packageName = packname; + String packageDirName = packageName.replace('.', '/'); + // 定义一个枚举的集合 并进行循环来处理这个目录下的things + Enumeration dirs; + try { +// dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName); + dirs = loader.getResources(packageDirName); + // 循环迭代下去 + while (dirs.hasMoreElements()) { + // 获取下一个元素 + URL url = dirs.nextElement(); + // 得到协议的名称 + String protocol = url.getProtocol(); + // 如果是以文件的形式保存在服务器上 + if ("file".equals(protocol)) { +// System.err.println("file类型的扫描"); + // 获取包的物理路径 + String filePath = URLDecoder.decode(url.getFile(), "UTF-8"); + // 以文件的方式扫描整个包下的文件 并添加到集合中 + findAndAddClassesInPackageByFile(packageName, filePath, + true, classes); + } else if ("jar".equals(protocol)) { + // 如果是jar包文件 + // 定义一个JarFile +// System.err.println("jar类型的扫描"); + JarFile jar; + try { + // 获取jar + jar = ((JarURLConnection) url.openConnection()) + .getJarFile(); + // 从此jar包 得到一个枚举类 + Enumeration entries = jar.entries(); + // 同样的进行循环迭代 + while (entries.hasMoreElements()) { + // 获取jar里的一个实体 可以是目录 和一些jar包里的其他文件 如META-INF等文件 + JarEntry entry = entries.nextElement(); + String name = entry.getName(); + // 如果是以/开头的 + if (name.charAt(0) == '/') { + // 获取后面的字符串 + name = name.substring(1); + } + // 如果前半部分和定义的包名相同 + if (name.startsWith(packageDirName)) { + int idx = name.lastIndexOf('/'); + // 如果以"/"结尾 是一个包 + if (idx != -1) { + // 获取包名 把"/"替换成"." + packageName = name.substring(0, idx) + .replace('/', '.'); + } + // 如果是一个.class文件 而且不是目录 + if (name.endsWith(".class") && !entry.isDirectory()) { + // 去掉后面的".class" 获取真正的类名 + String className = name.substring( + packageName.length() + 1, + name.length() - 6); + try { + // 添加到classes + classes.add(Class + .forName(packageName + '.' + + className)); + } catch (ClassNotFoundException e) { + // log + // .error("添加用户自定义视图类错误 找不到此类的.class文件"); +// e.printStackTrace(); + } + } + } + } + } catch (IOException e) { + // log.error("在扫描用户定义视图时从jar包获取文件出错"); +// e.printStackTrace(); + } + } + } + } catch (IOException e) { +// e.printStackTrace(); + } + + return classes; + } + + /** + * 以文件的形式来获取包下的所有Class + *

+ *

+ * 代码摘抄自 http://www.oschina.net/code/snippet_129830_8767

+ */ + private static void findAndAddClassesInPackageByFile(String packageName, + String packagePath, final boolean recursive, Set> classes) { + // 获取此包的目录 建立一个File + File dir = new File(packagePath); + // 如果不存在或者 也不是目录就直接返回 + if (!dir.exists() || !dir.isDirectory()) { + // log.warn("用户定义包名 " + packageName + " 下没有任何文件"); + return; + } + // 如果存在 就获取包下的所有文件 包括目录 + File[] dirfiles = dir.listFiles(new FileFilter() { + // 自定义过滤规则 如果可以循环(包含子目录) 或则是以.class结尾的文件(编译好的java类文件) + @Override + public boolean accept(File file) { + return (recursive && file.isDirectory()) + || (file.getName().endsWith(".class")); + } + }); + // 循环所有文件 + for (File file : dirfiles) { + // 如果是目录 则继续扫描 + if (file.isDirectory()) { + findAndAddClassesInPackageByFile( + packageName + "." + file.getName(), + file.getAbsolutePath(), recursive, classes); + } else { + // 如果是java类文件 去掉后面的.class 只留下类名 + String className = file.getName().substring(0, + file.getName().length() - 6); + try { + // 添加到集合中去 + // classes.add(Class.forName(packageName + '.' + + // className)); + // 经过回复同学的提醒,这里用forName有一些不好,会触发static方法,没有使用classLoader的load干净 + classes.add(Thread.currentThread().getContextClassLoader() + .loadClass(packageName + '.' + className)); + } catch (ClassNotFoundException e) { + // log.error("添加用户自定义视图类错误 找不到此类的.class文件"); +// e.printStackTrace(); + } + } + } + } + + /** + * 设置对象某个成员的值 + * + * @param field 属性对象 + * @param value 属性值 + * @param target 目标对象 + * @throws IllegalArgumentException 非法参数 + * @throws IllegalAccessException 非法进入 + */ + public static void set(Field field, Object value, Object target) throws IllegalArgumentException, IllegalAccessException { + final boolean isAccessible = field.isAccessible(); + try { + field.setAccessible(true); + field.set(target, value); + } finally { + field.setAccessible(isAccessible); + } + } + + /** + * 获取一个类下的所有成员(包括父类、私有成员) + * + * @param clazz 目标类 + * @return 类下所有属性 + */ + public static Set getFields(Class clazz) { + final Set fields = new LinkedHashSet(); + final Class parentClazz = clazz.getSuperclass(); + Collections.addAll(fields, clazz.getDeclaredFields()); + if (null != parentClazz) { + fields.addAll(getFields(parentClazz)); + } + return fields; + } + + /** + * 获取一个类下的指定成员 + * + * @param clazz 目标类 + * @param name 属性名 + * @return 属性 + */ + public static Field getField(Class clazz, String name) { + for (Field field : getFields(clazz)) { + if (ArthasCheckUtils.isEquals(field.getName(), name)) { + return field; + } + }//for + return null; + } + + /** + * 获取对象某个成员的值 + * + * @param + * @param target 目标对象 + * @param field 目标属性 + * @return 目标属性值 + * @throws IllegalArgumentException 非法参数 + * @throws IllegalAccessException 非法进入 + */ + public static T getFieldValueByField(Object target, Field field) throws IllegalArgumentException, IllegalAccessException { + final boolean isAccessible = field.isAccessible(); + try { + field.setAccessible(true); + //noinspection unchecked + return (T) field.get(target); + } finally { + field.setAccessible(isAccessible); + } + } + + /** + * 将字符串转换为指定类型,目前只支持9种类型:8种基本类型(包括其包装类)以及字符串 + * + * @param t 目标对象类型 + * @param value 目标值 + * @return 类型转换后的值 + */ + @SuppressWarnings("unchecked") + public static T valueOf(Class t, String value) { + if (ArthasCheckUtils.isIn(t, int.class, Integer.class)) { + return (T) Integer.valueOf(value); + } else if (ArthasCheckUtils.isIn(t, long.class, Long.class)) { + return (T) Long.valueOf(value); + } else if (ArthasCheckUtils.isIn(t, double.class, Double.class)) { + return (T) Double.valueOf(value); + } else if (ArthasCheckUtils.isIn(t, float.class, Float.class)) { + return (T) Float.valueOf(value); + } else if (ArthasCheckUtils.isIn(t, char.class, Character.class)) { + return (T) Character.valueOf(value.charAt(0)); + } else if (ArthasCheckUtils.isIn(t, byte.class, Byte.class)) { + return (T) Byte.valueOf(value); + } else if (ArthasCheckUtils.isIn(t, boolean.class, Boolean.class)) { + return (T) Boolean.valueOf(value); + } else if (ArthasCheckUtils.isIn(t, short.class, Short.class)) { + return (T) Short.valueOf(value); + } else if (ArthasCheckUtils.isIn(t, String.class)) { + return (T) value; + } else { + return null; + } + } + + + /** + * 定义类 + * + * @param targetClassLoader 目标classloader + * @param className 类名称 + * @param classByteArray 类字节码数组 + * @return 定义的类 + * @throws NoSuchMethodException + * @throws InvocationTargetException + * @throws IllegalAccessException + */ + public static Class defineClass( + final ClassLoader targetClassLoader, + final String className, + final byte[] classByteArray) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + + final Method defineClassMethod = ClassLoader.class.getDeclaredMethod( + "defineClass", + String.class, + byte[].class, + int.class, + int.class + ); + + synchronized (defineClassMethod) { + final boolean acc = defineClassMethod.isAccessible(); + try { + defineClassMethod.setAccessible(true); + return (Class) defineClassMethod.invoke( + targetClassLoader, + className, + classByteArray, + 0, + classByteArray.length + ); + } finally { + defineClassMethod.setAccessible(acc); + } + } + + + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/reflect/FieldUtils.java b/core/src/main/java/com/taobao/arthas/core/util/reflect/FieldUtils.java new file mode 100644 index 00000000..932cdc43 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/reflect/FieldUtils.java @@ -0,0 +1,451 @@ +package com.taobao.arthas.core.util.reflect; + +import com.taobao.arthas.core.util.StringUtils; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; + +/** + * @author ralf0131 2016-12-28 14:39. + */ +public class FieldUtils { + + private static final int ACCESS_TEST = Modifier.PUBLIC | Modifier.PROTECTED | Modifier.PRIVATE; + + /** + * Reads the named {@code public} {@link Field}. Only the class of the specified object will be considered. + * + * @param target + * the object to reflect, must not be {@code null} + * @param fieldName + * the field name to obtain + * @return the value of the field + * @throws IllegalArgumentException + * if {@code target} is {@code null}, or the field name is blank or empty or could not be found + * @throws IllegalAccessException + * if the named field is not {@code public} + */ + public static Object readDeclaredField(final Object target, final String fieldName) throws IllegalAccessException { + return readDeclaredField(target, fieldName, false); + } + + /** + * Gets a {@link Field} value by name. Only the class of the specified object will be considered. + * + * @param target + * the object to reflect, must not be {@code null} + * @param fieldName + * the field name to obtain + * @param forceAccess + * whether to break scope restrictions using the + * {@link java.lang.reflect.AccessibleObject#setAccessible(boolean)} method. {@code false} will only + * match public fields. + * @return the Field object + * @throws IllegalArgumentException + * if {@code target} is {@code null}, or the field name is blank or empty or could not be found + * @throws IllegalAccessException + * if the field is not made accessible + */ + public static Object readDeclaredField(final Object target, final String fieldName, final boolean forceAccess) throws IllegalAccessException { + isTrue(target != null, "target object must not be null"); + final Class cls = target.getClass(); + final Field field = getDeclaredField(cls, fieldName, forceAccess); + isTrue(field != null, "Cannot locate declared field %s.%s", cls, fieldName); + // already forced access above, don't repeat it here: + return readField(field, target, false); + } + + /** + * Gets an accessible {@link Field} by name, breaking scope if requested. Only the specified class will be + * considered. + * + * @param cls + * the {@link Class} to reflect, must not be {@code null} + * @param fieldName + * the field name to obtain + * @param forceAccess + * whether to break scope restrictions using the + * {@link java.lang.reflect.AccessibleObject#setAccessible(boolean)} method. {@code false} will only + * match {@code public} fields. + * @return the Field object + * @throws IllegalArgumentException + * if the class is {@code null}, or the field name is blank or empty + */ + public static Field getDeclaredField(final Class cls, final String fieldName, final boolean forceAccess) { + isTrue(cls != null, "The class must not be null"); + isTrue(!StringUtils.isBlank(fieldName), "The field name must not be blank/empty"); + try { + // only consider the specified class by using getDeclaredField() + final Field field = cls.getDeclaredField(fieldName); + if (!isAccessible(field)) { + if (forceAccess) { + field.setAccessible(true); + } else { + return null; + } + } + return field; + } catch (final NoSuchFieldException e) { // NOPMD + // ignore + } + return null; + } + + /** + * Reads a {@link Field}. + * + * @param field + * the field to use + * @param target + * the object to call on, may be {@code null} for {@code static} fields + * @param forceAccess + * whether to break scope restrictions using the + * {@link java.lang.reflect.AccessibleObject#setAccessible(boolean)} method. + * @return the field value + * @throws IllegalArgumentException + * if the field is {@code null} + * @throws IllegalAccessException + * if the field is not made accessible + */ + public static Object readField(final Field field, final Object target, final boolean forceAccess) throws IllegalAccessException { + isTrue(field != null, "The field must not be null"); + if (forceAccess && !field.isAccessible()) { + field.setAccessible(true); + } else { + setAccessibleWorkaround(field); + } + return field.get(target); + } + + /** + * Reads an accessible {@code static} {@link Field}. + * + * @param field + * to read + * @return the field value + * @throws IllegalArgumentException + * if the field is {@code null}, or not {@code static} + * @throws IllegalAccessException + * if the field is not accessible + */ + public static Object readStaticField(final Field field) throws IllegalAccessException { + return readStaticField(field, false); + } + + /** + * Reads a static {@link Field}. + * + * @param field + * to read + * @param forceAccess + * whether to break scope restrictions using the + * {@link java.lang.reflect.AccessibleObject#setAccessible(boolean)} method. + * @return the field value + * @throws IllegalArgumentException + * if the field is {@code null} or not {@code static} + * @throws IllegalAccessException + * if the field is not made accessible + */ + public static Object readStaticField(final Field field, final boolean forceAccess) throws IllegalAccessException { + isTrue(field != null, "The field must not be null"); + isTrue(Modifier.isStatic(field.getModifiers()), "The field '%s' is not static", field.getName()); + return readField(field, (Object) null, forceAccess); + } + + /** + * Writes a {@code public static} {@link Field}. + * + * @param field + * to write + * @param value + * to set + * @throws IllegalArgumentException + * if the field is {@code null} or not {@code static}, or {@code value} is not assignable + * @throws IllegalAccessException + * if the field is not {@code public} or is {@code final} + */ + public static void writeStaticField(final Field field, final Object value) throws IllegalAccessException { + writeStaticField(field, value, false); + } + + /** + * Writes a static {@link Field}. + * + * @param field + * to write + * @param value + * to set + * @param forceAccess + * whether to break scope restrictions using the + * {@link java.lang.reflect.AccessibleObject#setAccessible(boolean)} method. {@code false} will only + * match {@code public} fields. + * @throws IllegalArgumentException + * if the field is {@code null} or not {@code static}, or {@code value} is not assignable + * @throws IllegalAccessException + * if the field is not made accessible or is {@code final} + */ + public static void writeStaticField(final Field field, final Object value, final boolean forceAccess) throws IllegalAccessException { + isTrue(field != null, "The field must not be null"); + isTrue(Modifier.isStatic(field.getModifiers()), "The field %s.%s is not static", field.getDeclaringClass().getName(), + field.getName()); + writeField(field, (Object) null, value, forceAccess); + } + + /** + * Writes a {@link Field}. + * + * @param field + * to write + * @param target + * the object to call on, may be {@code null} for {@code static} fields + * @param value + * to set + * @param forceAccess + * whether to break scope restrictions using the + * {@link java.lang.reflect.AccessibleObject#setAccessible(boolean)} method. {@code false} will only + * match {@code public} fields. + * @throws IllegalArgumentException + * if the field is {@code null} or {@code value} is not assignable + * @throws IllegalAccessException + * if the field is not made accessible or is {@code final} + */ + public static void writeField(final Field field, final Object target, final Object value, final boolean forceAccess) + throws IllegalAccessException { + isTrue(field != null, "The field must not be null"); + if (forceAccess && !field.isAccessible()) { + field.setAccessible(true); + } else { + setAccessibleWorkaround(field); + } + field.set(target, value); + } + + /** + * Gets all fields of the given class and its parents (if any). + * + * @param cls + * the {@link Class} to query + * @return an array of Fields (possibly empty). + * @throws IllegalArgumentException + * if the class is {@code null} + * @since 3.2 + */ + public static Field[] getAllFields(final Class cls) { + final List allFieldsList = getAllFieldsList(cls); + return allFieldsList.toArray(new Field[allFieldsList.size()]); + } + + /** + * Gets all fields of the given class and its parents (if any). + * + * @param cls + * the {@link Class} to query + * @return an array of Fields (possibly empty). + * @throws IllegalArgumentException + * if the class is {@code null} + * @since 3.2 + */ + public static List getAllFieldsList(final Class cls) { + isTrue(cls != null, "The class must not be null"); + final List allFields = new ArrayList(); + Class currentClass = cls; + while (currentClass != null) { + final Field[] declaredFields = currentClass.getDeclaredFields(); + for (final Field field : declaredFields) { + allFields.add(field); + } + currentClass = currentClass.getSuperclass(); + } + return allFields; + } + + /** + * Gets an accessible {@link Field} by name respecting scope. Superclasses/interfaces will be considered. + * + * @param cls + * the {@link Class} to reflect, must not be {@code null} + * @param fieldName + * the field name to obtain + * @return the Field object + * @throws IllegalArgumentException + * if the class is {@code null}, or the field name is blank or empty + */ + public static Field getField(final Class cls, final String fieldName) { + final Field field = getField(cls, fieldName, false); + setAccessibleWorkaround(field); + return field; + } + + /** + * Gets an accessible {@link Field} by name, breaking scope if requested. Superclasses/interfaces will be + * considered. + * + * @param cls + * the {@link Class} to reflect, must not be {@code null} + * @param fieldName + * the field name to obtain + * @param forceAccess + * whether to break scope restrictions using the + * {@link java.lang.reflect.AccessibleObject#setAccessible(boolean)} method. {@code false} will only + * match {@code public} fields. + * @return the Field object + * @throws IllegalArgumentException + * if the class is {@code null}, or the field name is blank or empty or is matched at multiple places + * in the inheritance hierarchy + */ + public static Field getField(final Class cls, final String fieldName, final boolean forceAccess) { + isTrue(cls != null, "The class must not be null"); + isTrue(!StringUtils.isBlank(fieldName), "The field name must not be blank/empty"); + // FIXME is this workaround still needed? lang requires Java 6 + // Sun Java 1.3 has a bugged implementation of getField hence we write the + // code ourselves + + // getField() will return the Field object with the declaring class + // set correctly to the class that declares the field. Thus requesting the + // field on a subclass will return the field from the superclass. + // + // priority order for lookup: + // searchclass private/protected/package/public + // superclass protected/package/public + // private/different package blocks access to further superclasses + // implementedinterface public + + // check up the superclass hierarchy + for (Class acls = cls; acls != null; acls = acls.getSuperclass()) { + try { + final Field field = acls.getDeclaredField(fieldName); + // getDeclaredField checks for non-public scopes as well + // and it returns accurate results + if (!Modifier.isPublic(field.getModifiers())) { + if (forceAccess) { + field.setAccessible(true); + } else { + continue; + } + } + return field; + } catch (final NoSuchFieldException ex) { // NOPMD + // ignore + } + } + // check the public interface case. This must be manually searched for + // incase there is a public supersuperclass field hidden by a private/package + // superclass field. + Field match = null; + for (final Class class1 : getAllInterfaces(cls)) { + try { + final Field test = class1.getField(fieldName); + isTrue(match == null, "Reference to field %s is ambiguous relative to %s" + + "; a matching field exists on two or more implemented interfaces.", fieldName, cls); + match = test; + } catch (final NoSuchFieldException ex) { // NOPMD + // ignore + } + } + return match; + } + + /** + *

Gets a {@code List} of all interfaces implemented by the given + * class and its superclasses.

+ * + *

The order is determined by looking through each interface in turn as + * declared in the source file and following its hierarchy up. Then each + * superclass is considered in the same way. Later duplicates are ignored, + * so the order is maintained.

+ * + * @param cls the class to look up, may be {@code null} + * @return the {@code List} of interfaces in order, + * {@code null} if null input + */ + public static List> getAllInterfaces(final Class cls) { + if (cls == null) { + return null; + } + + final LinkedHashSet> interfacesFound = new LinkedHashSet>(); + getAllInterfaces(cls, interfacesFound); + + return new ArrayList>(interfacesFound); + } + + /** + * Get the interfaces for the specified class. + * + * @param cls the class to look up, may be {@code null} + * @param interfacesFound the {@code Set} of interfaces for the class + */ + private static void getAllInterfaces(Class cls, final HashSet> interfacesFound) { + while (cls != null) { + final Class[] interfaces = cls.getInterfaces(); + + for (final Class i : interfaces) { + if (interfacesFound.add(i)) { + getAllInterfaces(i, interfacesFound); + } + } + + cls = cls.getSuperclass(); + } + } + + + /** + * XXX Default access superclass workaround. + * + * When a {@code public} class has a default access superclass with {@code public} members, + * these members are accessible. Calling them from compiled code works fine. + * Unfortunately, on some JVMs, using reflection to invoke these members + * seems to (wrongly) prevent access even when the modifier is {@code public}. + * Calling {@code setAccessible(true)} solves the problem but will only work from + * sufficiently privileged code. Better workarounds would be gratefully + * accepted. + * @param o the AccessibleObject to set as accessible + * @return a boolean indicating whether the accessibility of the object was set to true. + */ + static boolean setAccessibleWorkaround(final AccessibleObject o) { + if (o == null || o.isAccessible()) { + return false; + } + final Member m = (Member) o; + if (!o.isAccessible() && Modifier.isPublic(m.getModifiers()) && isPackageAccess(m.getDeclaringClass().getModifiers())) { + try { + o.setAccessible(true); + return true; + } catch (final SecurityException e) { // NOPMD + // ignore in favor of subsequent IllegalAccessException + } + } + return false; + } + + /** + * Returns whether a given set of modifiers implies package access. + * @param modifiers to test + * @return {@code true} unless {@code package}/{@code protected}/{@code private} modifier detected + */ + static boolean isPackageAccess(final int modifiers) { + return (modifiers & ACCESS_TEST) == 0; + } + + /** + * Returns whether a {@link Member} is accessible. + * @param m Member to check + * @return {@code true} if m is accessible + */ + static boolean isAccessible(final Member m) { + return m != null && Modifier.isPublic(m.getModifiers()) && !m.isSynthetic(); + } + + static void isTrue(final boolean expression, final String message, final Object... values) { + if (expression == false) { + throw new IllegalArgumentException(String.format(message, values)); + } + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/util/usage/StyledUsageFormatter.java b/core/src/main/java/com/taobao/arthas/core/util/usage/StyledUsageFormatter.java new file mode 100644 index 00000000..458010c7 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/usage/StyledUsageFormatter.java @@ -0,0 +1,125 @@ +package com.taobao.arthas.core.util.usage; + +import com.taobao.middleware.cli.Argument; +import com.taobao.middleware.cli.CLI; +import com.taobao.middleware.cli.Option; +import com.taobao.middleware.cli.UsageMessageFormatter; +import com.taobao.text.Color; +import com.taobao.text.Decoration; +import com.taobao.text.Style; +import com.taobao.text.ui.TableElement; +import com.taobao.text.util.RenderUtil; + + +import java.util.Collections; + +import static com.taobao.text.ui.Element.row; +import static com.taobao.text.ui.Element.label; + +/** + * @author ralf0131 2016-12-14 22:16. + */ +public class StyledUsageFormatter extends UsageMessageFormatter { + + private Color fontColor; + + public StyledUsageFormatter(Color fontColor) { + this.fontColor = fontColor; + } + + public static String styledUsage(CLI cli, int width) { + StringBuilder usageBuilder = new StringBuilder(); + UsageMessageFormatter formatter = new StyledUsageFormatter(Color.green); + formatter.setWidth(width); + cli.usage(usageBuilder, formatter); + return usageBuilder.toString(); + } + + @Override + public void usage(StringBuilder builder, String prefix, CLI cli) { + + TableElement table = new TableElement(1, 2).leftCellPadding(1).rightCellPadding(1); + + table.add(row().add(label("USAGE:").style(getHighlightedStyle()))); + table.add(row().add(label(computeUsageLine(prefix, cli)))); + table.add(row().add("")); + table.add(row().add(label("SUMMARY:").style(getHighlightedStyle()))); + table.add(row().add(label(" " + cli.getSummary()))); + + if (cli.getDescription() != null) { + String[] descLines = cli.getDescription().split("\\n"); + for (String line: descLines) { + if (shouldBeHighlighted(line)) { + table.add(row().add(label(line).style(getHighlightedStyle()))); + } else { + table.add(row().add(label(line))); + } + } + } + + if (!cli.getOptions().isEmpty() || !cli.getArguments().isEmpty()) { + table.add(row().add("")); + table.row(label("OPTIONS:").style(getHighlightedStyle())); + for (Option option: cli.getOptions()) { + if (option.acceptValue()) { + table.add(row().add(label("-" + option.getShortName() + ", --" + option.getLongName() + " ") + .style(getHighlightedStyle())) + .add(option.getDescription())); + } else { + table.add(row().add(label("-" + option.getShortName() + ", --" + option.getLongName()) + .style(getHighlightedStyle())) + .add(option.getDescription())); + } + } + + for (Argument argument: cli.getArguments()) { + table.add(row().add(label("<" + argument.getArgName() + ">").style(getHighlightedStyle())) + .add(argument.getDescription())); + } + } + + builder.append(RenderUtil.render(table, getWidth())); + } + + private Style.Composite getHighlightedStyle() { + return Style.style(Decoration.bold, fontColor); + } + + private String computeUsageLine(String prefix, CLI cli) { + // initialise the string buffer + StringBuilder buff; + if (prefix == null) { + buff = new StringBuilder(" "); + } else { + buff = new StringBuilder(" ").append(prefix); + if (!prefix.endsWith(" ")) { + buff.append(" "); + } + } + + buff.append(cli.getName()).append(" "); + + if (getOptionComparator() != null) { + Collections.sort(cli.getOptions(), getOptionComparator()); + } + + // iterate over the options + for (Option option : cli.getOptions()) { + appendOption(buff, option); + buff.append(" "); + } + + // iterate over the arguments + for (Argument arg : cli.getArguments()) { + appendArgument(buff, arg, arg.isRequired()); + buff.append(" "); + } + + return buff.toString(); + } + + private boolean shouldBeHighlighted(String line) { + return !line.startsWith(" "); + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/view/Ansi.java b/core/src/main/java/com/taobao/arthas/core/view/Ansi.java new file mode 100644 index 00000000..54ad2075 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/view/Ansi.java @@ -0,0 +1,729 @@ +package com.taobao.arthas.core.view; + +import java.util.ArrayList; +import java.util.concurrent.Callable; + +/** + * Provides a fluent API for generating ANSI escape sequences. + * + * @author Hiram Chirino + * @since 1.0 + */ +public class Ansi { + + private static final char FIRST_ESC_CHAR = 27; + private static final char SECOND_ESC_CHAR = '['; + + public static enum Color { + BLACK(0, "BLACK"), + RED(1, "RED"), + GREEN(2, "GREEN"), + YELLOW(3, "YELLOW"), + BLUE(4, "BLUE"), + MAGENTA(5, "MAGENTA"), + CYAN(6, "CYAN"), + WHITE(7, "WHITE"), + DEFAULT(9, "DEFAULT"); + + private final int value; + private final String name; + + Color(int index, String name) { + this.value = index; + this.name = name; + } + + @Override + public String toString() { + return name; + } + + public int value() { + return value; + } + + public int fg() { + return value + 30; + } + + public int bg() { + return value + 40; + } + + public int fgBright() { + return value + 90; + } + + public int bgBright() { + return value + 100; + } + } + + ; + + public static enum Attribute { + RESET(0, "RESET"), + INTENSITY_BOLD(1, "INTENSITY_BOLD"), + INTENSITY_FAINT(2, "INTENSITY_FAINT"), + ITALIC(3, "ITALIC_ON"), + UNDERLINE(4, "UNDERLINE_ON"), + BLINK_SLOW(5, "BLINK_SLOW"), + BLINK_FAST(6, "BLINK_FAST"), + NEGATIVE_ON(7, "NEGATIVE_ON"), + CONCEAL_ON(8, "CONCEAL_ON"), + STRIKETHROUGH_ON(9, "STRIKETHROUGH_ON"), + UNDERLINE_DOUBLE(21, "UNDERLINE_DOUBLE"), + INTENSITY_BOLD_OFF(22, "INTENSITY_BOLD_OFF"), + ITALIC_OFF(23, "ITALIC_OFF"), + UNDERLINE_OFF(24, "UNDERLINE_OFF"), + BLINK_OFF(25, "BLINK_OFF"), + NEGATIVE_OFF(27, "NEGATIVE_OFF"), + CONCEAL_OFF(28, "CONCEAL_OFF"), + STRIKETHROUGH_OFF(29, "STRIKETHROUGH_OFF"); + + private final int value; + private final String name; + + Attribute(int index, String name) { + this.value = index; + this.name = name; + } + + @Override + public String toString() { + return name; + } + + public int value() { + return value; + } + + } + + ; + + public static enum Erase { + FORWARD(0, "FORWARD"), + BACKWARD(1, "BACKWARD"), + ALL(2, "ALL"); + + private final int value; + private final String name; + + Erase(int index, String name) { + this.value = index; + this.name = name; + } + + @Override + public String toString() { + return name; + } + + public int value() { + return value; + } + } + + ; + + public static final String DISABLE = Ansi.class.getName() + ".disable"; + + private static Callable detector = new Callable() { + public Boolean call() throws Exception { + return !Boolean.getBoolean(DISABLE); + } + }; + + public static void setDetector(final Callable detector) { + if (detector == null) throw new IllegalArgumentException(); + Ansi.detector = detector; + } + + public static boolean isDetected() { + try { + return detector.call(); + } catch (Exception e) { + return true; + } + } + + private static final InheritableThreadLocal holder = new InheritableThreadLocal() { + @Override + protected Boolean initialValue() { + return isDetected(); + } + }; + + public static void setEnabled(final boolean flag) { + holder.set(flag); + } + + public static boolean isEnabled() { + return holder.get(); + } + + public static Ansi ansi() { + if (isEnabled()) { + return new Ansi(); + } else { + return new NoAnsi(); + } + } + + public static Ansi ansi(StringBuilder builder) { + if (isEnabled()) { + return new Ansi(builder); + } else { + return new NoAnsi(builder); + } + } + + public static Ansi ansi(int size) { + if (isEnabled()) { + return new Ansi(size); + } else { + return new NoAnsi(size); + } + } + + private static class NoAnsi + extends Ansi { + public NoAnsi() { + super(); + } + + public NoAnsi(int size) { + super(size); + } + + public NoAnsi(StringBuilder builder) { + super(builder); + } + + @Override + public Ansi fg(Color color) { + return this; + } + + @Override + public Ansi bg(Color color) { + return this; + } + + @Override + public Ansi fgBright(Color color) { + return this; + } + + @Override + public Ansi bgBright(Color color) { + return this; + } + + @Override + public Ansi a(Attribute attribute) { + return this; + } + + @Override + public Ansi cursor(int x, int y) { + return this; + } + + @Override + public Ansi cursorToColumn(int x) { + return this; + } + + @Override + public Ansi cursorUp(int y) { + return this; + } + + @Override + public Ansi cursorRight(int x) { + return this; + } + + @Override + public Ansi cursorDown(int y) { + return this; + } + + @Override + public Ansi cursorLeft(int x) { + return this; + } + + @Override + public Ansi cursorDownLine() { + return this; + } + + @Override + public Ansi cursorDownLine(final int n) { + return this; + } + + @Override + public Ansi cursorUpLine() { + return this; + } + + @Override + public Ansi cursorUpLine(final int n) { + return this; + } + + @Override + public Ansi eraseScreen() { + return this; + } + + @Override + public Ansi eraseScreen(Erase kind) { + return this; + } + + @Override + public Ansi eraseLine() { + return this; + } + + @Override + public Ansi eraseLine(Erase kind) { + return this; + } + + @Override + public Ansi scrollUp(int rows) { + return this; + } + + @Override + public Ansi scrollDown(int rows) { + return this; + } + + @Override + public Ansi saveCursorPosition() { + return this; + } + + @Override + @Deprecated + public Ansi restorCursorPosition() { + return this; + } + + @Override + public Ansi restoreCursorPosition() { + return this; + } + + @Override + public Ansi reset() { + return this; + } + } + + private final StringBuilder builder; + private final ArrayList attributeOptions = new ArrayList(5); + + public Ansi() { + this(new StringBuilder()); + } + + public Ansi(Ansi parent) { + this(new StringBuilder(parent.builder)); + attributeOptions.addAll(parent.attributeOptions); + } + + public Ansi(int size) { + this(new StringBuilder(size)); + } + + public Ansi(StringBuilder builder) { + this.builder = builder; + } + + public Ansi fg(Color color) { + attributeOptions.add(color.fg()); + return this; + } + + public Ansi fgBlack() { + return this.fg(Color.BLACK); + } + + public Ansi fgBlue() { + return this.fg(Color.BLUE); + } + + public Ansi fgCyan() { + return this.fg(Color.CYAN); + } + + public Ansi fgDefault() { + return this.fg(Color.DEFAULT); + } + + public Ansi fgGreen() { + return this.fg(Color.GREEN); + } + + public Ansi fgMagenta() { + return this.fg(Color.MAGENTA); + } + + public Ansi fgRed() { + return this.fg(Color.RED); + } + + public Ansi fgYellow() { + return this.fg(Color.YELLOW); + } + + public Ansi bg(Color color) { + attributeOptions.add(color.bg()); + return this; + } + + public Ansi bgCyan() { + return this.fg(Color.CYAN); + } + + public Ansi bgDefault() { + return this.bg(Color.DEFAULT); + } + + public Ansi bgGreen() { + return this.bg(Color.GREEN); + } + + public Ansi bgMagenta() { + return this.bg(Color.MAGENTA); + } + + public Ansi bgRed() { + return this.bg(Color.RED); + } + + public Ansi bgYellow() { + return this.bg(Color.YELLOW); + } + + public Ansi fgBright(Color color) { + attributeOptions.add(color.fgBright()); + return this; + } + + public Ansi fgBrightBlack() { + return this.fgBright(Color.BLACK); + } + + public Ansi fgBrightBlue() { + return this.fgBright(Color.BLUE); + } + + public Ansi fgBrightCyan() { + return this.fgBright(Color.CYAN); + } + + public Ansi fgBrightDefault() { + return this.fgBright(Color.DEFAULT); + } + + public Ansi fgBrightGreen() { + return this.fgBright(Color.GREEN); + } + + public Ansi fgBrightMagenta() { + return this.fgBright(Color.MAGENTA); + } + + public Ansi fgBrightRed() { + return this.fgBright(Color.RED); + } + + public Ansi fgBrightYellow() { + return this.fgBright(Color.YELLOW); + } + + public Ansi bgBright(Color color) { + attributeOptions.add(color.bgBright()); + return this; + } + + public Ansi bgBrightCyan() { + return this.fgBright(Color.CYAN); + } + + public Ansi bgBrightDefault() { + return this.bgBright(Color.DEFAULT); + } + + public Ansi bgBrightGreen() { + return this.bgBright(Color.GREEN); + } + + public Ansi bgBrightMagenta() { + return this.bg(Color.MAGENTA); + } + + public Ansi bgBrightRed() { + return this.bgBright(Color.RED); + } + + public Ansi bgBrightYellow() { + return this.bgBright(Color.YELLOW); + } + + public Ansi a(Attribute attribute) { + attributeOptions.add(attribute.value()); + return this; + } + + public Ansi cursor(final int x, final int y) { + return appendEscapeSequence('H', x, y); + } + + public Ansi cursorToColumn(final int x) { + return appendEscapeSequence('G', x); + } + + public Ansi cursorUp(final int y) { + return appendEscapeSequence('A', y); + } + + public Ansi cursorDown(final int y) { + return appendEscapeSequence('B', y); + } + + public Ansi cursorRight(final int x) { + return appendEscapeSequence('C', x); + } + + public Ansi cursorLeft(final int x) { + return appendEscapeSequence('D', x); + } + + public Ansi cursorDownLine() { + return appendEscapeSequence('E'); + } + + public Ansi cursorDownLine(final int n) { + return appendEscapeSequence('E', n); + } + + public Ansi cursorUpLine() { + return appendEscapeSequence('F'); + } + + public Ansi cursorUpLine(final int n) { + return appendEscapeSequence('F', n); + } + + public Ansi eraseScreen() { + return appendEscapeSequence('J', Erase.ALL.value()); + } + + public Ansi eraseScreen(final Erase kind) { + return appendEscapeSequence('J', kind.value()); + } + + public Ansi eraseLine() { + return appendEscapeSequence('K'); + } + + public Ansi eraseLine(final Erase kind) { + return appendEscapeSequence('K', kind.value()); + } + + public Ansi scrollUp(final int rows) { + return appendEscapeSequence('S', rows); + } + + public Ansi scrollDown(final int rows) { + return appendEscapeSequence('T', rows); + } + + public Ansi saveCursorPosition() { + return appendEscapeSequence('s'); + } + + @Deprecated + public Ansi restorCursorPosition() { + return appendEscapeSequence('u'); + } + + public Ansi restoreCursorPosition() { + return appendEscapeSequence('u'); + } + + public Ansi reset() { + return a(Attribute.RESET); + } + + public Ansi bold() { + return a(Attribute.INTENSITY_BOLD); + } + + public Ansi boldOff() { + return a(Attribute.INTENSITY_BOLD_OFF); + } + + public Ansi a(String value) { + flushAttributes(); + builder.append(value); + return this; + } + + public Ansi a(boolean value) { + flushAttributes(); + builder.append(value); + return this; + } + + public Ansi a(char value) { + flushAttributes(); + builder.append(value); + return this; + } + + public Ansi a(char[] value, int offset, int len) { + flushAttributes(); + builder.append(value, offset, len); + return this; + } + + public Ansi a(char[] value) { + flushAttributes(); + builder.append(value); + return this; + } + + public Ansi a(CharSequence value, int start, int end) { + flushAttributes(); + builder.append(value, start, end); + return this; + } + + public Ansi a(CharSequence value) { + flushAttributes(); + builder.append(value); + return this; + } + + public Ansi a(double value) { + flushAttributes(); + builder.append(value); + return this; + } + + public Ansi a(float value) { + flushAttributes(); + builder.append(value); + return this; + } + + public Ansi a(int value) { + flushAttributes(); + builder.append(value); + return this; + } + + public Ansi a(long value) { + flushAttributes(); + builder.append(value); + return this; + } + + public Ansi a(Object value) { + flushAttributes(); + builder.append(value); + return this; + } + + public Ansi a(StringBuffer value) { + flushAttributes(); + builder.append(value); + return this; + } + + public Ansi newline() { + flushAttributes(); + builder.append(System.getProperty("line.separator")); + return this; + } + + public Ansi format(String pattern, Object... args) { + flushAttributes(); + builder.append(String.format(pattern, args)); + return this; + } + + + @Override + public String toString() { + flushAttributes(); + return builder.toString(); + } + + /////////////////////////////////////////////////////////////////// + // Private Helper Methods + /////////////////////////////////////////////////////////////////// + + private Ansi appendEscapeSequence(char command) { + flushAttributes(); + builder.append(FIRST_ESC_CHAR); + builder.append(SECOND_ESC_CHAR); + builder.append(command); + return this; + } + + private Ansi appendEscapeSequence(char command, int option) { + flushAttributes(); + builder.append(FIRST_ESC_CHAR); + builder.append(SECOND_ESC_CHAR); + builder.append(option); + builder.append(command); + return this; + } + + private Ansi appendEscapeSequence(char command, Object... options) { + flushAttributes(); + return _appendEscapeSequence(command, options); + } + + private void flushAttributes() { + if (attributeOptions.isEmpty()) + return; + if (attributeOptions.size() == 1 && attributeOptions.get(0) == 0) { + builder.append(FIRST_ESC_CHAR); + builder.append(SECOND_ESC_CHAR); + builder.append('m'); + } else { + _appendEscapeSequence('m', attributeOptions.toArray()); + } + attributeOptions.clear(); + } + + private Ansi _appendEscapeSequence(char command, Object... options) { + builder.append(FIRST_ESC_CHAR); + builder.append(SECOND_ESC_CHAR); + int size = options.length; + for (int i = 0; i < size; i++) { + if (i != 0) { + builder.append(';'); + } + if (options[i] != null) { + builder.append(options[i]); + } + } + builder.append(command); + return this; + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/taobao/arthas/core/view/ClassInfoView.java b/core/src/main/java/com/taobao/arthas/core/view/ClassInfoView.java new file mode 100644 index 00000000..6b9d2eee --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/view/ClassInfoView.java @@ -0,0 +1,194 @@ +package com.taobao.arthas.core.view; + +import com.taobao.arthas.core.util.Constants; +import com.taobao.arthas.core.util.StringUtils; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.security.CodeSource; + +/** + * Java类信息控件 + * Created by vlinux on 15/5/7. + */ +public class ClassInfoView implements View { + + private final Class clazz; + private final boolean isPrintField; + private final int width; + + public ClassInfoView(Class clazz, boolean isPrintField, int width) { + this.clazz = clazz; + this.isPrintField = isPrintField; + this.width = width; + } + + @Override + public String draw() { + return drawClassInfo(); + } + + private String getCodeSource(final CodeSource cs) { + if (null == cs + || null == cs.getLocation() + || null == cs.getLocation().getFile()) { + return Constants.EMPTY_STRING; + } + + return cs.getLocation().getFile(); + } + + private String drawClassInfo() { + final CodeSource cs = clazz.getProtectionDomain().getCodeSource(); + + final TableView view = new TableView(new TableView.ColumnDefine[]{ + new TableView.ColumnDefine("isAnonymousClass".length(), false, TableView.Align.RIGHT), + // (列数-1) * 3 + 4 = 7 + new TableView.ColumnDefine(width - "isAnonymousClass".length() - 7, false, TableView.Align.LEFT) + }) + .addRow("class-info", StringUtils.classname(clazz)) + .addRow("code-source", getCodeSource(cs)) + .addRow("name", StringUtils.classname(clazz)) + .addRow("isInterface", clazz.isInterface()) + .addRow("isAnnotation", clazz.isAnnotation()) + .addRow("isEnum", clazz.isEnum()) + .addRow("isAnonymousClass", clazz.isAnonymousClass()) + .addRow("isArray", clazz.isArray()) + .addRow("isLocalClass", clazz.isLocalClass()) + .addRow("isMemberClass", clazz.isMemberClass()) + .addRow("isPrimitive", clazz.isPrimitive()) + .addRow("isSynthetic", clazz.isSynthetic()) + .addRow("simple-name", clazz.getSimpleName()) + .addRow("modifier", StringUtils.modifier(clazz.getModifiers(), ',')) + .addRow("annotation", drawAnnotation()) + .addRow("interfaces", drawInterface()) + .addRow("super-class", drawSuperClass()) + .addRow("class-loader", drawClassLoader()); + + if (isPrintField) { + view.addRow("fields", drawField()); + } + + return view.hasBorder(true).padding(1).draw(); + } + + + private String drawField() { + + final StringBuilder fieldSB = new StringBuilder(); + + final Field[] fields = clazz.getDeclaredFields(); + if (null != fields + && fields.length > 0) { + + for (Field field : fields) { + + final KVView kvView = new KVView(new TableView.ColumnDefine(TableView.Align.RIGHT), new TableView.ColumnDefine(50, false, TableView.Align.LEFT)) + .add("modifier", StringUtils.modifier(field.getModifiers(), ',')) + .add("type", StringUtils.classname(field.getType())) + .add("name", field.getName()); + + + final StringBuilder annotationSB = new StringBuilder(); + final Annotation[] annotationArray = field.getAnnotations(); + if (null != annotationArray && annotationArray.length > 0) { + for (Annotation annotation : annotationArray) { + annotationSB.append(StringUtils.classname(annotation.annotationType())).append(","); + } + if (annotationSB.length() > 0) { + annotationSB.deleteCharAt(annotationSB.length() - 1); + } + kvView.add("annotation", annotationSB); + } + + + if (Modifier.isStatic(field.getModifiers())) { + final boolean isAccessible = field.isAccessible(); + try { + field.setAccessible(true); + kvView.add("value", StringUtils.objectToString(field.get(null))); + } catch (IllegalAccessException e) { + // + } finally { + field.setAccessible(isAccessible); + } + }//if + + fieldSB.append(kvView.draw()).append("\n"); + + }//for + + } + + return fieldSB.toString(); + } + + private String drawAnnotation() { + final StringBuilder annotationSB = new StringBuilder(); + final Annotation[] annotationArray = clazz.getDeclaredAnnotations(); + + if (null != annotationArray && annotationArray.length > 0) { + for (Annotation annotation : annotationArray) { + annotationSB.append(StringUtils.classname(annotation.annotationType())).append(","); + } + if (annotationSB.length() > 0) { + annotationSB.deleteCharAt(annotationSB.length() - 1); + } + } else { + annotationSB.append(Constants.EMPTY_STRING); + } + + return annotationSB.toString(); + } + + private String drawInterface() { + final StringBuilder interfaceSB = new StringBuilder(); + final Class[] interfaceArray = clazz.getInterfaces(); + if (null == interfaceArray || interfaceArray.length == 0) { + interfaceSB.append(Constants.EMPTY_STRING); + } else { + for (Class i : interfaceArray) { + interfaceSB.append(i.getName()).append(","); + } + if (interfaceSB.length() > 0) { + interfaceSB.deleteCharAt(interfaceSB.length() - 1); + } + } + return interfaceSB.toString(); + } + + private String drawSuperClass() { + final LadderView ladderView = new LadderView(); + Class superClass = clazz.getSuperclass(); + if (null != superClass) { + ladderView.addItem(StringUtils.classname(superClass)); + while (true) { + superClass = superClass.getSuperclass(); + if (null == superClass) { + break; + } + ladderView.addItem(StringUtils.classname(superClass)); + }//while + } + return ladderView.draw(); + } + + + private String drawClassLoader() { + final LadderView ladderView = new LadderView(); + ClassLoader loader = clazz.getClassLoader(); + if (null != loader) { + ladderView.addItem(loader.toString()); + while (true) { + loader = loader.getParent(); + if (null == loader) { + break; + } + ladderView.addItem(loader.toString()); + } + } + return ladderView.draw(); + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/view/KVView.java b/core/src/main/java/com/taobao/arthas/core/view/KVView.java new file mode 100644 index 00000000..7ba33818 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/view/KVView.java @@ -0,0 +1,57 @@ +package com.taobao.arthas.core.view; + +import com.taobao.arthas.core.util.StringUtils; + +import java.util.Scanner; + +/** + * KV排版控件 + * Created by vlinux on 15/5/9. + */ +public class KVView implements View { + + private final TableView tableView; + + public KVView() { + this.tableView = new TableView(new TableView.ColumnDefine[]{ + new TableView.ColumnDefine(TableView.Align.RIGHT), + new TableView.ColumnDefine(TableView.Align.RIGHT), + new TableView.ColumnDefine(TableView.Align.LEFT) + }) + .hasBorder(false) + .padding(0); + } + + public KVView(TableView.ColumnDefine keyColumnDefine, TableView.ColumnDefine valueColumnDefine) { + this.tableView = new TableView(new TableView.ColumnDefine[]{ + keyColumnDefine, + new TableView.ColumnDefine(TableView.Align.RIGHT), + valueColumnDefine + }) + .hasBorder(false) + .padding(0); + } + + public KVView add(final Object key, final Object value) { + tableView.addRow(key, " : ", value); + return this; + } + + @Override + public String draw() { + String content = tableView.draw(); + StringBuilder sb = new StringBuilder(); + // 清理多余的空格 + Scanner scanner = new Scanner(content); + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + if (line != null) { + //清理一行后面多余的空格 + line = StringUtils.stripEnd(line, " "); + } + sb.append(line).append('\n'); + } + scanner.close(); + return sb.toString(); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/view/LadderView.java b/core/src/main/java/com/taobao/arthas/core/view/LadderView.java new file mode 100644 index 00000000..4d2f6e1c --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/view/LadderView.java @@ -0,0 +1,65 @@ +package com.taobao.arthas.core.view; + +import com.taobao.arthas.core.util.StringUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * 阶梯缩进控件 + * Created by vlinux on 15/5/8. + */ +public class LadderView implements View { + + // 分隔符 + private static final String LADDER_CHAR = "`-"; + + // 缩进符 + private static final String STEP_CHAR = " "; + + // 缩进长度 + private static final int INDENT_STEP = 2; + + private final List items = new ArrayList(); + + + @Override + public String draw() { + final StringBuilder ladderSB = new StringBuilder(); + int deep = 0; + for (String item : items) { + + // 第一个条目不需要分隔符 + if (deep == 0) { + ladderSB + .append(item) + .append("\n"); + } + + // 其他的需要添加分隔符 + else { + ladderSB + .append(StringUtils.repeat(STEP_CHAR, deep * INDENT_STEP)) + .append(LADDER_CHAR) + .append(item) + .append("\n"); + } + + deep++; + + } + return ladderSB.toString(); + } + + /** + * 添加一个项目 + * + * @param item 项目 + * @return this + */ + public LadderView addItem(String item) { + items.add(item); + return this; + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/view/MethodInfoView.java b/core/src/main/java/com/taobao/arthas/core/view/MethodInfoView.java new file mode 100644 index 00000000..755e7e11 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/view/MethodInfoView.java @@ -0,0 +1,91 @@ +package com.taobao.arthas.core.view; + +import com.taobao.arthas.core.util.Constants; +import com.taobao.arthas.core.util.StringUtils; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + + +/** + * Java方法信息控件 + * Created by vlinux on 15/5/9. + */ +public class MethodInfoView implements View { + + private final Method method; + private final int width; + + public MethodInfoView(Method method, int width) { + this.method = method; + this.width = width; + } + + @Override + public String draw() { + return new TableView(new TableView.ColumnDefine[]{ + new TableView.ColumnDefine("declaring-class".length(), false, TableView.Align.RIGHT), + // (列数-1) * 3 + 4 = 7 + new TableView.ColumnDefine(width - "declaring-class".length() - 7, false, TableView.Align.LEFT) + }) + .addRow("declaring-class", method.getDeclaringClass().getName()) + .addRow("method-name", method.getName()) + .addRow("modifier", StringUtils.modifier(method.getModifiers(), ',')) + .addRow("annotation", drawAnnotation()) + .addRow("parameters", drawParameters()) + .addRow("return", drawReturn()) + .addRow("exceptions", drawExceptions()) + .padding(1) + .hasBorder(true) + .draw(); + } + + private String drawAnnotation() { + + final StringBuilder annotationSB = new StringBuilder(); + final Annotation[] annotationArray = method.getDeclaredAnnotations(); + + if (null != annotationArray && annotationArray.length > 0) { + for (Annotation annotation : annotationArray) { + annotationSB.append(StringUtils.classname(annotation.annotationType())).append(","); + } + if (annotationSB.length() > 0) { + annotationSB.deleteCharAt(annotationSB.length() - 1); + } + } else { + annotationSB.append(Constants.EMPTY_STRING); + } + + return annotationSB.toString(); + } + + private String drawParameters() { + final StringBuilder paramsSB = new StringBuilder(); + final Class[] paramTypes = method.getParameterTypes(); + if (null != paramTypes && paramTypes.length > 0) { + for (Class clazz : paramTypes) { + paramsSB.append(StringUtils.classname(clazz)).append("\n"); + } + } + return paramsSB.toString(); + } + + private String drawReturn() { + final StringBuilder returnSB = new StringBuilder(); + final Class returnTypeClass = method.getReturnType(); + returnSB.append(StringUtils.classname(returnTypeClass)).append("\n"); + return returnSB.toString(); + } + + private String drawExceptions() { + final StringBuilder exceptionSB = new StringBuilder(); + final Class[] exceptionTypes = method.getExceptionTypes(); + if (null != exceptionTypes && exceptionTypes.length > 0) { + for (Class clazz : exceptionTypes) { + exceptionSB.append(StringUtils.classname(clazz)).append("\n"); + } + } + return exceptionSB.toString(); + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/view/ObjectView.java b/core/src/main/java/com/taobao/arthas/core/view/ObjectView.java new file mode 100644 index 00000000..6076c631 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/view/ObjectView.java @@ -0,0 +1,662 @@ +package com.taobao.arthas.core.view; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.serializer.SerializerFeature; +import com.taobao.arthas.core.GlobalOptions; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.Field; +import java.text.SimpleDateFormat; +import java.util.*; + +import static java.lang.String.format; + +/** + * 对象控件
+ * 能展示出一个对象的内部结构 + * Created by vlinux on 15/5/20. + */ +public class ObjectView implements View { + + private final static int MAX_OBJECT_LENGTH = 10 * 1024 * 1024; // 10M + + private final Object object; + private final int deep; + private final int maxObjectLength; + + public ObjectView(Object object, int deep) { + this(object, deep, MAX_OBJECT_LENGTH); + } + + public ObjectView(Object object, int deep, int maxObjectLength) { + this.object = object; + this.deep = deep > 4 ? 4 : deep; + this.maxObjectLength = maxObjectLength; + } + + @Override + public String draw() { + StringBuilder buf = new StringBuilder(); + try { + if (GlobalOptions.isUsingJson) { + return JSON.toJSONString(object, SerializerFeature.IgnoreErrorGetter); + } + renderObject(object, 0, deep, buf); + return buf.toString(); + } catch (ObjectTooLargeException e) { + buf.append(" Object size exceeds size limit: ") + .append(maxObjectLength) + .append(", try to specify -M size_limit in your command, check the help command for more."); + return buf.toString(); + } catch (Throwable t) { + return "ERROR DATA!!!"; + } + } + + private final static String TAB = " "; + + private final static Map ASCII_MAP = new HashMap(); + + static { + ASCII_MAP.put((byte) 0, "NUL"); + ASCII_MAP.put((byte) 1, "SOH"); + ASCII_MAP.put((byte) 2, "STX"); + ASCII_MAP.put((byte) 3, "ETX"); + ASCII_MAP.put((byte) 4, "EOT"); + ASCII_MAP.put((byte) 5, "ENQ"); + ASCII_MAP.put((byte) 6, "ACK"); + ASCII_MAP.put((byte) 7, "BEL"); + ASCII_MAP.put((byte) 8, "BS"); + ASCII_MAP.put((byte) 9, "HT"); + ASCII_MAP.put((byte) 10, "LF"); + ASCII_MAP.put((byte) 11, "VT"); + ASCII_MAP.put((byte) 12, "FF"); + ASCII_MAP.put((byte) 13, "CR"); + ASCII_MAP.put((byte) 14, "SO"); + ASCII_MAP.put((byte) 15, "SI"); + ASCII_MAP.put((byte) 16, "DLE"); + ASCII_MAP.put((byte) 17, "DC1"); + ASCII_MAP.put((byte) 18, "DC2"); + ASCII_MAP.put((byte) 19, "DC3"); + ASCII_MAP.put((byte) 20, "DC4"); + ASCII_MAP.put((byte) 21, "NAK"); + ASCII_MAP.put((byte) 22, "SYN"); + ASCII_MAP.put((byte) 23, "ETB"); + ASCII_MAP.put((byte) 24, "CAN"); + ASCII_MAP.put((byte) 25, "EM"); + ASCII_MAP.put((byte) 26, "SUB"); + ASCII_MAP.put((byte) 27, "ESC"); + ASCII_MAP.put((byte) 28, "FS"); + ASCII_MAP.put((byte) 29, "GS"); + ASCII_MAP.put((byte) 30, "RS"); + ASCII_MAP.put((byte) 31, "US"); + ASCII_MAP.put((byte) 127, "DEL"); + } + + private void renderObject(Object obj, int deep, int expand, final StringBuilder buf) throws ObjectTooLargeException { + + if (null == obj) { + appendStringBuilder(buf,"null"); + } else { + + final Class clazz = obj.getClass(); + final String className = clazz.getSimpleName(); + + // 7种基础类型,直接输出@类型[值] + if (Integer.class.isInstance(obj) + || Long.class.isInstance(obj) + || Float.class.isInstance(obj) + || Double.class.isInstance(obj) + // || Character.class.isInstance(obj) + || Short.class.isInstance(obj) + || Byte.class.isInstance(obj) + || Boolean.class.isInstance(obj)) { + appendStringBuilder(buf, format("@%s[%s]", className, obj)); + } + + // Char要特殊处理,因为有不可见字符的因素 + else if (Character.class.isInstance(obj)) { + + final Character c = (Character) obj; + + // ASCII的可见字符 + if (c >= 32 + && c <= 126) { + appendStringBuilder(buf, format("@%s[%s]", className, c)); + } + + // ASCII的控制字符 + else if (ASCII_MAP.containsKey((byte) c.charValue())) { + appendStringBuilder(buf, format("@%s[%s]", className, ASCII_MAP.get((byte) c.charValue()))); + } + + // 超过ASCII的编码范围 + else { + appendStringBuilder(buf, format("@%s[%s]", className, c)); + } + + } + + // 字符串类型单独处理 + else if (String.class.isInstance(obj)) { + appendStringBuilder(buf, "@"); + appendStringBuilder(buf, className); + appendStringBuilder(buf, "["); + for (Character c : ((String) obj).toCharArray()) { + switch (c) { + case '\n': + appendStringBuilder(buf, "\\n"); + break; + case '\r': + appendStringBuilder(buf, "\\r"); + break; + default: + appendStringBuilder(buf, c.toString()); + }//switch + }//for + appendStringBuilder(buf, "]"); + } + + // 集合类输出 + else if (Collection.class.isInstance(obj)) { + + @SuppressWarnings("unchecked") final Collection collection = (Collection) obj; + + // 非根节点或空集合只展示摘要信息 + if (!isExpand(deep, expand) + || collection.isEmpty()) { + + appendStringBuilder(buf, format("@%s[isEmpty=%s;size=%d]", + className, + collection.isEmpty(), + collection.size())); + } + + // 展开展示 + else { + appendStringBuilder(buf, format("@%s[", className)); + for (Object e : collection) { + appendStringBuilder(buf, "\n"); + for (int i = 0; i < deep+1; i++) { + appendStringBuilder(buf, TAB); + } + renderObject(e, deep + 1, expand, buf); + appendStringBuilder(buf, ","); + } + appendStringBuilder(buf, "\n"); + for (int i = 0; i < deep; i++) { + appendStringBuilder(buf, TAB); + } + appendStringBuilder(buf, "]"); + } + + } + + + // Map类输出 + else if (Map.class.isInstance(obj)) { + @SuppressWarnings("unchecked") final Map map = (Map) obj; + + // 非根节点或空集合只展示摘要信息 + if (!isExpand(deep, expand) + || map.isEmpty()) { + + appendStringBuilder(buf, format("@%s[isEmpty=%s;size=%d]", + className, + map.isEmpty(), + map.size())); + + } else { + appendStringBuilder(buf, format("@%s[", className)); + for (Map.Entry entry : map.entrySet()) { + appendStringBuilder(buf, "\n"); + for (int i = 0; i < deep+1; i++) { + appendStringBuilder(buf, TAB); + } + renderObject(entry.getKey(), deep + 1, expand, buf); + appendStringBuilder(buf, ":"); + renderObject(entry.getValue(), deep + 1, expand, buf); + appendStringBuilder(buf, ","); + } + appendStringBuilder(buf, "\n"); + for (int i = 0; i < deep; i++) { + appendStringBuilder(buf, TAB); + } + appendStringBuilder(buf, "]"); + } + } + + + // 数组类输出 + else if (obj.getClass().isArray()) { + + + final String typeName = obj.getClass().getSimpleName(); + + // int[] + if (typeName.equals("int[]")) { + + final int[] arrays = (int[]) obj; + // 非根节点或空集合只展示摘要信息 + if (!isExpand(deep, expand) + || arrays.length == 0) { + + appendStringBuilder(buf, format("@%s[isEmpty=%s;size=%d]", + typeName, + arrays.length == 0, + arrays.length)); + + } + + // 展开展示 + else { + appendStringBuilder(buf, format("@%s[", className)); + for (int e : arrays) { + appendStringBuilder(buf, "\n"); + for (int i = 0; i < deep+1; i++) { + appendStringBuilder(buf, TAB); + } + renderObject(e, deep + 1, expand, buf); + appendStringBuilder(buf, ","); + } + appendStringBuilder(buf, "\n"); + for (int i = 0; i < deep; i++) { + appendStringBuilder(buf, TAB); + } + appendStringBuilder(buf, "]"); + } + + } + + // long[] + else if (typeName.equals("long[]")) { + + final long[] arrays = (long[]) obj; + // 非根节点或空集合只展示摘要信息 + if (!isExpand(deep, expand) + || arrays.length == 0) { + + appendStringBuilder(buf, format("@%s[isEmpty=%s;size=%d]", + typeName, + arrays.length == 0, + arrays.length)); + + } + + // 展开展示 + else { + appendStringBuilder(buf, format("@%s[", className)); + for (long e : arrays) { + appendStringBuilder(buf, "\n"); + for (int i = 0; i < deep+1; i++) { + appendStringBuilder(buf, TAB); + } + renderObject(e, deep + 1, expand, buf); + appendStringBuilder(buf, ","); + } + appendStringBuilder(buf, "\n"); + for (int i = 0; i < deep; i++) { + appendStringBuilder(buf, TAB); + } + appendStringBuilder(buf, "]"); + } + + } + + // short[] + else if (typeName.equals("short[]")) { + + final short[] arrays = (short[]) obj; + // 非根节点或空集合只展示摘要信息 + if (!isExpand(deep, expand) + || arrays.length == 0) { + + appendStringBuilder(buf, format("@%s[isEmpty=%s;size=%d]", + typeName, + arrays.length == 0, + arrays.length)); + + } + + // 展开展示 + else { + appendStringBuilder(buf, format("@%s[", className)); + for (short e : arrays) { + appendStringBuilder(buf, "\n"); + for (int i = 0; i < deep+1; i++) { + appendStringBuilder(buf, TAB); + } + renderObject(e, deep + 1, expand, buf); + appendStringBuilder(buf, ","); + } + appendStringBuilder(buf, "\n"); + for (int i = 0; i < deep; i++) { + appendStringBuilder(buf, TAB); + } + appendStringBuilder(buf, "]"); + } + + } + + // float[] + else if (typeName.equals("float[]")) { + + final float[] arrays = (float[]) obj; + // 非根节点或空集合只展示摘要信息 + if (!isExpand(deep, expand) + || arrays.length == 0) { + + appendStringBuilder(buf, format("@%s[isEmpty=%s;size=%d]", + typeName, + arrays.length == 0, + arrays.length)); + + } + + // 展开展示 + else { + appendStringBuilder(buf, format("@%s[", className)); + for (float e : arrays) { + appendStringBuilder(buf, "\n"); + for (int i = 0; i < deep+1; i++) { + appendStringBuilder(buf, TAB); + } + renderObject(e, deep + 1, expand, buf); + appendStringBuilder(buf, ","); + } + appendStringBuilder(buf, "\n"); + for (int i = 0; i < deep; i++) { + appendStringBuilder(buf, TAB); + } + appendStringBuilder(buf, "]"); + } + + } + + // double[] + else if (typeName.equals("double[]")) { + + final double[] arrays = (double[]) obj; + // 非根节点或空集合只展示摘要信息 + if (!isExpand(deep, expand) + || arrays.length == 0) { + + appendStringBuilder(buf, format("@%s[isEmpty=%s;size=%d]", + typeName, + arrays.length == 0, + arrays.length)); + + } + + // 展开展示 + else { + appendStringBuilder(buf, format("@%s[", className)); + for (double e : arrays) { + appendStringBuilder(buf, "\n"); + for (int i = 0; i < deep+1; i++) { + appendStringBuilder(buf, TAB); + } + renderObject(e, deep + 1, expand, buf); + appendStringBuilder(buf, ","); + } + appendStringBuilder(buf, "\n"); + for (int i = 0; i < deep; i++) { + appendStringBuilder(buf, TAB); + } + appendStringBuilder(buf, "]"); + } + + } + + // boolean[] + else if (typeName.equals("boolean[]")) { + + final boolean[] arrays = (boolean[]) obj; + // 非根节点或空集合只展示摘要信息 + if (!isExpand(deep, expand) + || arrays.length == 0) { + + appendStringBuilder(buf, format("@%s[isEmpty=%s;size=%d]", + typeName, + arrays.length == 0, + arrays.length)); + + } + + // 展开展示 + else { + appendStringBuilder(buf, format("@%s[", className)); + for (boolean e : arrays) { + appendStringBuilder(buf, "\n"); + for (int i = 0; i < deep+1; i++) { + appendStringBuilder(buf, TAB); + } + renderObject(e, deep + 1, expand, buf); + appendStringBuilder(buf, ","); + } + appendStringBuilder(buf, "\n"); + for (int i = 0; i < deep; i++) { + appendStringBuilder(buf, TAB); + } + appendStringBuilder(buf, "]"); + } + + } + + // char[] + else if (typeName.equals("char[]")) { + + final char[] arrays = (char[]) obj; + // 非根节点或空集合只展示摘要信息 + if (!isExpand(deep, expand) + || arrays.length == 0) { + + appendStringBuilder(buf, format("@%s[isEmpty=%s;size=%d]", + typeName, + arrays.length == 0, + arrays.length)); + + } + + // 展开展示 + else { + appendStringBuilder(buf, format("@%s[", className)); + for (char e : arrays) { + appendStringBuilder(buf, "\n"); + for (int i = 0; i < deep+1; i++) { + appendStringBuilder(buf, TAB); + } + renderObject(e, deep + 1, expand, buf); + appendStringBuilder(buf, ","); + } + appendStringBuilder(buf, "\n"); + for (int i = 0; i < deep; i++) { + appendStringBuilder(buf, TAB); + } + appendStringBuilder(buf, "]"); + } + + } + + // byte[] + else if (typeName.equals("byte[]")) { + + final byte[] arrays = (byte[]) obj; + // 非根节点或空集合只展示摘要信息 + if (!isExpand(deep, expand) + || arrays.length == 0) { + + appendStringBuilder(buf, format("@%s[isEmpty=%s;size=%d]", + typeName, + arrays.length == 0, + arrays.length)); + + } + + // 展开展示 + else { + appendStringBuilder(buf, format("@%s[", className)); + for (byte e : arrays) { + appendStringBuilder(buf, "\n"); + for (int i = 0; i < deep+1; i++) { + appendStringBuilder(buf, TAB); + } + renderObject(e, deep + 1, expand, buf); + appendStringBuilder(buf, ","); + } + appendStringBuilder(buf, "\n"); + for (int i = 0; i < deep; i++) { + appendStringBuilder(buf, TAB); + } + appendStringBuilder(buf, "]"); + } + + } + + // Object[] + else { + final Object[] arrays = (Object[]) obj; + // 非根节点或空集合只展示摘要信息 + if (!isExpand(deep, expand) + || arrays.length == 0) { + + appendStringBuilder(buf, format("@%s[isEmpty=%s;size=%d]", + typeName, + arrays.length == 0, + arrays.length)); + + } + + // 展开展示 + else { + appendStringBuilder(buf, format("@%s[", className)); + for (Object e : arrays) { + appendStringBuilder(buf, "\n"); + for (int i = 0; i < deep+1; i++) { + appendStringBuilder(buf, TAB); + } + renderObject(e, deep + 1, expand, buf); + appendStringBuilder(buf, ","); + } + appendStringBuilder(buf, "\n"); + for (int i = 0; i < deep; i++) { + appendStringBuilder(buf, TAB); + } + appendStringBuilder(buf, "]"); + } + } + + } + + + // Throwable输出 + else if (Throwable.class.isInstance(obj)) { + + if (!isExpand(deep, expand)) { + appendStringBuilder(buf, format("@%s[%s]", className, obj)); + } else { + + final Throwable throwable = (Throwable) obj; + final StringWriter sw = new StringWriter(); + final PrintWriter pw = new PrintWriter(sw); + throwable.printStackTrace(pw); + appendStringBuilder(buf, sw.toString()); + } + + } + + // Date输出 + else if (Date.class.isInstance(obj)) { + appendStringBuilder(buf, format("@%s[%s]", className, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS").format(obj))); + } + + // 普通Object输出 + else { + + if (!isExpand(deep, expand)) { + appendStringBuilder(buf, format("@%s[%s]", className, obj)); + } else { + appendStringBuilder(buf, format("@%s[", className)); + final Field[] fields = obj.getClass().getDeclaredFields(); + if (null != fields) { + for (Field field : fields) { + + field.setAccessible(true); + + try { + + final Object value = field.get(obj); + + appendStringBuilder(buf, "\n"); + for (int i = 0; i < deep+1; i++) { + appendStringBuilder(buf, TAB); + } + appendStringBuilder(buf, field.getName()); + appendStringBuilder(buf, "="); + renderObject(value, deep + 1, expand, buf); + appendStringBuilder(buf, ","); + + } catch (ObjectTooLargeException t) { + buf.append("..."); + break; + } catch (Throwable t) { + // ignore + } + }//for + appendStringBuilder(buf, "\n"); + }//if + for (int i = 0; i < deep; i++) { + appendStringBuilder(buf, TAB); + } + appendStringBuilder(buf, "]"); + } + + } + } + } + + /** + * 是否根节点 + * + * @param deep 深度 + * @return true:根节点 / false:非根节点 + */ + private static boolean isRoot(int deep) { + return deep == 0; + } + + + /** + * 是否展开当前深度的节点 + * + * @param deep 当前节点的深度 + * @param expand 展开极限 + * @return true:当前节点需要展开 / false:当前节点不需要展开 + */ + private static boolean isExpand(int deep, int expand) { + return deep < expand; + } + + /** + * append string to a string builder, with upper limit check + * @param buf the StringBuilder buffer + * @param data the data to be appended + * @throws ObjectTooLargeException if the size has exceeded the upper limit + */ + private void appendStringBuilder(StringBuilder buf, String data) throws ObjectTooLargeException { + if (buf.length() + data.length() > maxObjectLength) { + throw new ObjectTooLargeException("Object size exceeds size limit: " + maxObjectLength); + } + buf.append(data); + } + + private static class ObjectTooLargeException extends Exception { + + public ObjectTooLargeException(String message) { + super(message); + } + } + + +} diff --git a/core/src/main/java/com/taobao/arthas/core/view/TableView.java b/core/src/main/java/com/taobao/arthas/core/view/TableView.java new file mode 100644 index 00000000..5d70ad13 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/view/TableView.java @@ -0,0 +1,374 @@ +package com.taobao.arthas.core.view; + +import com.taobao.arthas.core.util.Constants; +import com.taobao.arthas.core.util.StringUtils; + +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +import static java.lang.Math.abs; +import static java.lang.Math.max; +import static java.lang.String.format; + +/** + * 表格控件 + * Created by vlinux on 15/5/7. + */ +public class TableView implements View { + + /** + * 上边框 + */ + public static final int BORDER_TOP = 1; + + /** + * 下边框 + */ + public static final int BORDER_BOTTOM = 1 << 1; + + // 各个列的定义 + private final ColumnDefine[] columnDefineArray; + + // 是否渲染边框 + private boolean hasBorder; + + // 边框 + private int borders = BORDER_TOP | BORDER_BOTTOM; + + // 内填充 + private int padding; + + public TableView(ColumnDefine[] columnDefineArray) { + this.columnDefineArray = null == columnDefineArray + ? new ColumnDefine[0] + : columnDefineArray; + } + + public TableView(int columnNum) { + this.columnDefineArray = new ColumnDefine[columnNum]; + for (int index = 0; index < this.columnDefineArray.length; index++) { + columnDefineArray[index] = new ColumnDefine(); + } + } + + private boolean isAnyBorder(int... borders) { + if (null == borders) { + return false; + } + for (int b : borders) { + if ((this.borders & b) == b) { + return true; + } + } + return false; + } + + /** + * 获取表格边框设置 + * + * @return 边框位 + */ + public int borders() { + return borders; + } + + /** + * 设置表格边框 + * + * @param border 边框位 + * @return this + */ + public TableView borders(int border) { + this.borders = border; + return this; + } + + @Override + public String draw() { + final StringBuilder tableSB = new StringBuilder(); + + // init width cache + final int[] widthCacheArray = new int[getColumnCount()]; + for (int index = 0; index < widthCacheArray.length; index++) { + widthCacheArray[index] = abs(columnDefineArray[index].getWidth()); + } + + final int tableHigh = getTableHigh(); + for (int rowIndex = 0; rowIndex < tableHigh; rowIndex++) { + + final boolean isFirstRow = rowIndex == 0; + final boolean isLastRow = rowIndex == tableHigh - 1; + + // 打印首分隔行 + if (isFirstRow + && hasBorder() + && isAnyBorder(BORDER_TOP)) { + tableSB.append(drawSeparationLine(widthCacheArray)).append("\n"); + } + + // 打印内部分割行 + if (!isFirstRow + && hasBorder()) { + tableSB.append(drawSeparationLine(widthCacheArray)).append("\n"); + } + + // 绘一行 + tableSB.append(drawRow(widthCacheArray, rowIndex)); + + + // 打印结尾分隔行 + if (isLastRow + && hasBorder() + && isAnyBorder(BORDER_BOTTOM)) { + // 打印分割行 + tableSB.append(drawSeparationLine(widthCacheArray)).append("\n"); + } + + } + + + return tableSB.toString(); + } + + + private String drawRow(int[] widthCacheArray, int rowIndex) { + + final StringBuilder rowSB = new StringBuilder(); + final Scanner[] scannerArray = new Scanner[getColumnCount()]; + try { + boolean hasNext; + do { + + hasNext = false; + final StringBuilder segmentSB = new StringBuilder(); + + for (int colIndex = 0; colIndex < getColumnCount(); colIndex++) { + + + final String borderChar = hasBorder() ? "|" : Constants.EMPTY_STRING; + final int width = widthCacheArray[colIndex]; + final boolean isLastColOfRow = colIndex == widthCacheArray.length - 1; + + + if (null == scannerArray[colIndex]) { + scannerArray[colIndex] = new Scanner( + new StringReader(StringUtils.wrap(getData(rowIndex, columnDefineArray[colIndex]), width))); + } + final Scanner scanner = scannerArray[colIndex]; + + final String data; + if (scanner.hasNext()) { + data = scanner.nextLine(); + hasNext = true; + } else { + data = Constants.EMPTY_STRING; + } + + if (width > 0) { + final ColumnDefine columnDefine = columnDefineArray[colIndex]; + final String dataFormat = getDataFormat(columnDefine, width); + final String paddingChar = StringUtils.repeat(" ", padding); + segmentSB.append(format(borderChar + paddingChar + dataFormat + paddingChar, data)); + } + + if (isLastColOfRow) { + segmentSB.append(borderChar).append("\n"); + } + + } + + if (hasNext) { + rowSB.append(segmentSB); + } + + } while (hasNext); + + return rowSB.toString(); + } finally { + for (Scanner scanner : scannerArray) { + if (null != scanner) { + scanner.close(); + } + } + } + + } + + private String getData(int rowIndex, ColumnDefine columnDefine) { + return columnDefine.getHigh() <= rowIndex + ? Constants.EMPTY_STRING + : columnDefine.dataList.get(rowIndex); + } + + private String getDataFormat(ColumnDefine columnDefine, int width) { + switch (columnDefine.align) { + case RIGHT: { + return "%" + width + "s"; + } + case LEFT: + default: { + return "%-" + width + "s"; + } + } + } + + /* + * 获取表格高度 + */ + private int getTableHigh() { + int tableHigh = 0; + for (ColumnDefine columnDefine : columnDefineArray) { + tableHigh = max(tableHigh, columnDefine.getHigh()); + } + return tableHigh; + } + + /* + * 打印分隔行 + */ + private String drawSeparationLine(int[] widthCacheArray) { + final StringBuilder separationLineSB = new StringBuilder(); + for (int width : widthCacheArray) { + if (width > 0) { + separationLineSB.append("+").append(StringUtils.repeat("-", width + 2 * padding)); + } + } + return separationLineSB + .append("+") + .toString(); + } + + /** + * 添加数据行 + * + * @param columnDataArray 数据数组 + */ + public TableView addRow(Object... columnDataArray) { + if (null == columnDataArray) { + return this; + } + + for (int index = 0; index < columnDefineArray.length; index++) { + final ColumnDefine columnDefine = columnDefineArray[index]; + if (index < columnDataArray.length + && null != columnDataArray[index]) { + columnDefine.dataList.add(StringUtils.replace(columnDataArray[index].toString(), "\t", " ")); + } else { + columnDefine.dataList.add(Constants.EMPTY_STRING); + } + } + + return this; + } + + + /** + * 对齐方向 + */ + public enum Align { + LEFT, + RIGHT + } + + /** + * 列定义 + */ + public static class ColumnDefine { + + private final int width; + private final boolean isAutoResize; + private final Align align; + private final List dataList = new ArrayList(); + + public ColumnDefine(int width, boolean isAutoResize, Align align) { + this.width = width; + this.isAutoResize = isAutoResize; + this.align = align; + } + + public ColumnDefine(Align align) { + this(0, true, align); + } + + public ColumnDefine() { + this(Align.LEFT); + } + + /** + * 获取当前列的宽度 + * + * @return 宽度 + */ + public int getWidth() { + + if (!isAutoResize) { + return width; + } + + int maxWidth = 0; + for (String data : dataList) { + final Scanner scanner = new Scanner(new StringReader(data)); + try { + while (scanner.hasNext()) { + maxWidth = max(StringUtils.length(scanner.nextLine()), maxWidth); + } + } finally { + scanner.close(); + } + } + + return maxWidth; + } + + /** + * 获取当前列的高度 + * + * @return 高度 + */ + public int getHigh() { + return dataList.size(); + } + + } + + /** + * 设置是否画边框 + * + * @param hasBorder true / false + */ + public TableView hasBorder(boolean hasBorder) { + this.hasBorder = hasBorder; + return this; + } + + /** + * 是否画边框 + * + * @return true / false + */ + public boolean hasBorder() { + return hasBorder; + } + + /** + * 设置内边距大小 + * + * @param padding 内边距 + */ + public TableView padding(int padding) { + this.padding = padding; + return this; + } + + /** + * 获取表格列总数 + * + * @return 表格列总数 + */ + public int getColumnCount() { + return columnDefineArray.length; + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/view/TreeView.java b/core/src/main/java/com/taobao/arthas/core/view/TreeView.java new file mode 100644 index 00000000..6c81cc2e --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/view/TreeView.java @@ -0,0 +1,316 @@ +package com.taobao.arthas.core.view; + +import com.taobao.arthas.core.util.StringUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 树形控件 + * Created by vlinux on 15/5/26. + */ +public class TreeView implements View { + + private static final String STEP_FIRST_CHAR = "`---"; + private static final String STEP_NORMAL_CHAR = "+---"; + private static final String STEP_HAS_BOARD = "| "; + private static final String STEP_EMPTY_BOARD = " "; + private static final String TIME_UNIT = "ms"; + + // 是否输出耗时 + private final boolean isPrintCost; + + // 根节点 + private final Node root; + + // 当前节点 + private Node current; + + // 最耗时的节点 + private Node maxCost; + + + public TreeView(boolean isPrintCost, String title) { + this.root = new Node(title).markBegin().markEnd(); + this.current = root; + this.isPrintCost = isPrintCost; + } + + @Override + public String draw() { + + findMaxCostNode(root); + + final StringBuilder treeSB = new StringBuilder(); + + final Ansi highlighted = Ansi.ansi().fg(Ansi.Color.RED); + + recursive(0, true, "", root, new Callback() { + + @Override + public void callback(int deep, boolean isLast, String prefix, Node node) { + treeSB.append(prefix).append(isLast ? STEP_FIRST_CHAR : STEP_NORMAL_CHAR); + if (isPrintCost && !node.isRoot()) { + if (node == maxCost) { + // the node with max cost will be highlighted + treeSB.append(highlighted.a(node.toString()).reset().toString()); + } else { + treeSB.append(node.toString()); + } + } + treeSB.append(node.data); + if (!StringUtils.isBlank(node.mark)) { + treeSB.append(" [").append(node.mark).append(node.marks > 1 ? "," + node.marks : "").append("]"); + } + treeSB.append("\n"); + } + + }); + + return treeSB.toString(); + } + + /** + * 递归遍历 + */ + private void recursive(int deep, boolean isLast, String prefix, Node node, Callback callback) { + callback.callback(deep, isLast, prefix, node); + if (!node.isLeaf()) { + final int size = node.children.size(); + for (int index = 0; index < size; index++) { + final boolean isLastFlag = index == size - 1; + final String currentPrefix = isLast ? prefix + STEP_EMPTY_BOARD : prefix + STEP_HAS_BOARD; + recursive( + deep + 1, + isLastFlag, + currentPrefix, + node.children.get(index), + callback + ); + } + } + } + + /** + * 查找耗时最大的节点,便于后续高亮展示 + * @param node + */ + private void findMaxCostNode(Node node) { + if (!node.isRoot() && !node.parent.isRoot()) { + if (maxCost == null) { + maxCost = node; + } else if (maxCost.totalCost < node.totalCost) { + maxCost = node; + } + } + if (!node.isLeaf()) { + for (Node n: node.children) { + findMaxCostNode(n); + } + } + } + + + /** + * 创建一个分支节点 + * + * @param data 节点数据 + * @return this + */ + public TreeView begin(String data) { + Node n = current.find(data); + if (n != null) { + current = n; + } else { + current = new Node(current, data); + } + current.markBegin(); + return this; + } + + /** + * 结束一个分支节点 + * + * @return this + */ + public TreeView end() { + if (current.isRoot()) { + throw new IllegalStateException("current node is root."); + } + current.markEnd(); + current = current.parent; + return this; + } + + /** + * 结束一个分支节点,并带上备注 + * + * @return this + */ + public TreeView end(String mark) { + if (current.isRoot()) { + throw new IllegalStateException("current node is root."); + } + current.markEnd().mark(mark); + current = current.parent; + return this; + } + + + /** + * 树节点 + */ + private static class Node { + + /** + * 父节点 + */ + final Node parent; + + /** + * 节点数据 + */ + final String data; + + /** + * 子节点 + */ + final List children = new ArrayList(); + + final Map map = new HashMap(); + + /** + * 开始时间戳 + */ + private long beginTimestamp; + + /** + * 结束时间戳 + */ + private long endTimestamp; + + /** + * 备注 + */ + private String mark; + + /** + * 构造树节点(根节点) + */ + private Node(String data) { + this.parent = null; + this.data = data; + } + + /** + * 构造树节点 + * + * @param parent 父节点 + * @param data 节点数据 + */ + private Node(Node parent, String data) { + this.parent = parent; + this.data = data; + parent.children.add(this); + parent.map.put(data, this); + } + + /** + * 查找已经存在的节点 + */ + Node find(String data) { + return map.get(data); + } + + /** + * 是否根节点 + * + * @return true / false + */ + boolean isRoot() { + return null == parent; + } + + /** + * 是否叶子节点 + * + * @return true / false + */ + boolean isLeaf() { + return children.isEmpty(); + } + + Node markBegin() { + beginTimestamp = System.nanoTime(); + return this; + } + + Node markEnd() { + endTimestamp = System.nanoTime(); + + long cost = getCost(); + if (cost < minCost) { + minCost = cost; + } + if (cost > maxCost) { + maxCost = cost; + } + times++; + totalCost += cost; + + return this; + } + + Node mark(String mark) { + this.mark = mark; + marks++; + return this; + } + + long getCost() { + return endTimestamp - beginTimestamp; + } + + /** + * convert nano-seconds to milli-seconds + */ + double getCostInMillis(long nanoSeconds) { + return nanoSeconds / 1000000.0; + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + if (times <= 1) { + sb.append("[").append(getCostInMillis(getCost())).append(TIME_UNIT).append("] "); + } else { + sb.append("[min=").append(getCostInMillis(minCost)).append(TIME_UNIT).append(",max=") + .append(getCostInMillis(maxCost)).append(TIME_UNIT).append(",total=") + .append(getCostInMillis(totalCost)).append(TIME_UNIT).append(",count=") + .append(times).append("] "); + } + return sb.toString(); + } + + /** + * 合并统计相同调用,并计算最小\最大\总耗时 + */ + private long minCost = Long.MAX_VALUE; + private long maxCost = Long.MIN_VALUE; + private long totalCost = 0; + private long times = 0; + private long marks = 0; + } + + + /** + * 遍历回调接口 + */ + private interface Callback { + + void callback(int deep, boolean isLast, String prefix, Node node); + + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/view/View.java b/core/src/main/java/com/taobao/arthas/core/view/View.java new file mode 100644 index 00000000..73e80d5f --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/view/View.java @@ -0,0 +1,14 @@ +package com.taobao.arthas.core.view; + +/** + * 命令行控件
+ * Created by vlinux on 15/5/7. + */ +public interface View { + + /** + * 输出外观 + */ + String draw(); + +} \ No newline at end of file diff --git a/core/src/main/resources/com/taobao/arthas/core/res/logo.txt b/core/src/main/resources/com/taobao/arthas/core/res/logo.txt new file mode 100644 index 00000000..7c3ce152 --- /dev/null +++ b/core/src/main/resources/com/taobao/arthas/core/res/logo.txt @@ -0,0 +1,30 @@ + ,---. + / O \ +| .-. | +| | | | +`--' `--' +,------. +| .--. ' +| '--'.' +| |\ \ +`--' '--' +,--------. +'--. .--' + | | + | | + `--' +,--. ,--. +| '--' | +| .--. | +| | | | +`--' `--' + ,---. + / O \ +| .-. | +| | | | +`--' `--' + ,---. +' .-' +`. `-. +.-' | +`-----' \ No newline at end of file diff --git a/core/src/main/resources/com/taobao/arthas/core/res/thanks.txt b/core/src/main/resources/com/taobao/arthas/core/res/thanks.txt new file mode 100644 index 00000000..a0781194 --- /dev/null +++ b/core/src/main/resources/com/taobao/arthas/core/res/thanks.txt @@ -0,0 +1,36 @@ + +PLATINUM DEVELOPERS + + vlinux + email : oldmanpushcart@gmail.com + weibo : http://weibo.com/vlinux + + chengtd + email : chengtongda@163.com + weibo : http://weibo.com/chengtd + + JieChenCN + weibo : http://weibo.com/471760204 + blog : http://xapp.in + + JamesPan + email : panjiabang@gmail.com + blog : http://blog.jamespan.me + + jotcmd + github : https://github.com/jotcmd + + hengyunabc + github : https://github.com/hengyunabc + + huxing.zhang + email : huxing.zhang@gmail.com + + zhuyong + email : diecui1202@gmail.com + + +ABOUT + + Thank you very much. + diff --git a/core/src/main/resources/com/taobao/arthas/core/shell/term/readline/inputrc b/core/src/main/resources/com/taobao/arthas/core/shell/term/readline/inputrc new file mode 100644 index 00000000..4f1792dd --- /dev/null +++ b/core/src/main/resources/com/taobao/arthas/core/shell/term/readline/inputrc @@ -0,0 +1,23 @@ +# In accordance with the Emacs/GNU Bash readline keyboard shortcuts +# http://www.catonmat.net/download/readline-emacs-editing-mode-cheat-sheet.pdf +# https://www.gnu.org/software/bash/manual/html_node/Commands-For-Killing.html + +"\C-a": beginning-of-line +"\C-e": end-of-line +"\C-f": forward-word +"\C-b": backward-word +"\e[D": backward-char +"\e[C": forward-char +"\e[B": next-history +"\e[A": previous-history +"\C-h": backward-delete-char +"\C-?": backward-delete-char +"\C-u": undo +"\C-d": delete-char +"\C-k": kill-line +"\C-i": complete +"\C-j": accept-line +"\C-m": accept-line +"\C-w": backward-delete-word +"\C-x\e[3~": backward-kill-line +"\e\C-?": backward-kill-word \ No newline at end of file diff --git a/core/src/test/java/com/taobao/arthas/core/view/ObjectViewTest.java b/core/src/test/java/com/taobao/arthas/core/view/ObjectViewTest.java new file mode 100644 index 00000000..55c9365f --- /dev/null +++ b/core/src/test/java/com/taobao/arthas/core/view/ObjectViewTest.java @@ -0,0 +1,267 @@ +package com.taobao.arthas.core.view; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * @author ralf0131 2018-07-10 10:55. + */ +public class ObjectViewTest { + + @Test + public void testNull() { + ObjectView objectView = new ObjectView(null, 3); + Assert.assertEquals("null", objectView.draw()); + } + + @Test + public void testInteger() { + ObjectView objectView = new ObjectView(new Integer(1), 3); + Assert.assertEquals("@Integer[1]", objectView.draw()); + } + + @Test + public void testChar() { + ObjectView objectView = new ObjectView(new Character('中'), 3); + Assert.assertEquals("@Character[中]", objectView.draw()); + } + + @Test + public void testString() { + ObjectView objectView = new ObjectView("hello\nworld!", 3); + Assert.assertEquals("@String[hello\\nworld!]", objectView.draw()); + } + + @Test + public void testList() { + List data = new ArrayList(); + data.add("aaa"); + data.add("bbb"); + ObjectView objectView = new ObjectView(data, 3); + String expected = "@ArrayList[\n" + + " @String[aaa],\n" + + " @String[bbb],\n" + + "]"; + Assert.assertEquals(expected, objectView.draw()); + } + + @Test + public void testMap() { + Map data = new LinkedHashMap(); + data.put("key1", "value1"); + data.put("key2", "value2"); + ObjectView objectView = new ObjectView(data, 3); + String expected = "@LinkedHashMap[\n" + + " @String[key1]:@String[value1],\n" + + " @String[key2]:@String[value2],\n" + + "]"; + Assert.assertEquals(expected, objectView.draw()); + } + + @Test + public void testIntArray() { + int[] data = {1,3,4,5}; + ObjectView objectView = new ObjectView(data, 3); + String expected = "@int[][\n" + + " @Integer[1],\n" + + " @Integer[3],\n" + + " @Integer[4],\n" + + " @Integer[5],\n" + + "]"; + Assert.assertEquals(expected, objectView.draw()); + } + + @Test + public void testLongArray() { + long[] data = {1L,3L,4L,5L}; + ObjectView objectView = new ObjectView(data, 3); + String expected = "@long[][\n" + + " @Long[1],\n" + + " @Long[3],\n" + + " @Long[4],\n" + + " @Long[5],\n" + + "]"; + Assert.assertEquals(expected, objectView.draw()); + } + + @Test + public void testShortArray() { + short[] data = {1,3,4,5}; + ObjectView objectView = new ObjectView(data, 3); + String expected = "@short[][\n" + + " @Short[1],\n" + + " @Short[3],\n" + + " @Short[4],\n" + + " @Short[5],\n" + + "]"; + Assert.assertEquals(expected, objectView.draw()); + } + + @Test + public void testFloatArray() { + float[] data = {1.0f, 3.0f, 4.2f, 5.3f}; + ObjectView objectView = new ObjectView(data, 3); + String expected = "@float[][\n" + + " @Float[1.0],\n" + + " @Float[3.0],\n" + + " @Float[4.2],\n" + + " @Float[5.3],\n" + + "]"; + Assert.assertEquals(expected, objectView.draw()); + } + + @Test + public void testDoubleArray() { + double[] data = {1.0d, 3.0d, 4.2d, 5.3d}; + ObjectView objectView = new ObjectView(data, 3); + String expected = "@double[][\n" + + " @Double[1.0],\n" + + " @Double[3.0],\n" + + " @Double[4.2],\n" + + " @Double[5.3],\n" + + "]"; + Assert.assertEquals(expected, objectView.draw()); + } + + @Test + public void testBooleanArray() { + boolean[] data = {true, false, true, true}; + ObjectView objectView = new ObjectView(data, 3); + String expected = "@boolean[][\n" + + " @Boolean[true],\n" + + " @Boolean[false],\n" + + " @Boolean[true],\n" + + " @Boolean[true],\n" + + "]"; + Assert.assertEquals(expected, objectView.draw()); + } + + @Test + public void testCharArray() { + char[] data = {'a', 'b', 'c', 'd'}; + ObjectView objectView = new ObjectView(data, 3); + String expected = "@char[][\n" + + " @Character[a],\n" + + " @Character[b],\n" + + " @Character[c],\n" + + " @Character[d],\n" + + "]"; + Assert.assertEquals(expected, objectView.draw()); + } + + @Test + public void testByteArray() { + byte[] data = {'a', 'b', 'c', 'd'}; + ObjectView objectView = new ObjectView(data, 3); + String expected = "@byte[][\n" + + " @Byte[97],\n" + + " @Byte[98],\n" + + " @Byte[99],\n" + + " @Byte[100],\n" + + "]"; + Assert.assertEquals(expected, objectView.draw()); + } + + @Test + public void testObjectArray() { + String[] data = {"111", "222", "333", "444"}; + ObjectView objectView = new ObjectView(data, 3); + String expected = "@String[][\n" + + " @String[111],\n" + + " @String[222],\n" + + " @String[333],\n" + + " @String[444],\n" + + "]"; + Assert.assertEquals(expected, objectView.draw()); + } + + @Test + public void testThrowable() { + Exception t = new Exception("test"); + ObjectView objectView = new ObjectView(t, 3); + Assert.assertTrue(objectView.draw().startsWith("java.lang.Exception: test")); + } + + @Test + public void testDate() { + Date d = new Date(1531204354961L); + ObjectView objectView = new ObjectView(d, 3); + String expected = "@Date[2018-07-10 14:32:34,961]"; + Assert.assertEquals(expected, objectView.draw()); + } + + @Test + public void testNestedClass() { + ObjectView objectView = new ObjectView(new NestedClass(100), 3); + + String expected = "@NestedClass[\n" + + " code=@Integer[100],\n" + + " c1=@NestedClass[\n" + + " code=@Integer[1],\n" + + " c1=@NestedClass[\n" + + " code=@Integer[1],\n" + + " c1=@NestedClass[com.taobao.arthas.core.view.ObjectViewTest$NestedClass@ffffffff],\n" + + " c2=@NestedClass[com.taobao.arthas.core.view.ObjectViewTest$NestedClass@ffffffff],\n" + + " ],\n" + + " c2=@NestedClass[\n" + + " code=@Integer[2],\n" + + " c1=@NestedClass[com.taobao.arthas.core.view.ObjectViewTest$NestedClass@ffffffff],\n" + + " c2=@NestedClass[com.taobao.arthas.core.view.ObjectViewTest$NestedClass@ffffffff],\n" + + " ],\n" + + " ],\n" + + " c2=@NestedClass[\n" + + " code=@Integer[2],\n" + + " c1=@NestedClass[\n" + + " code=@Integer[1],\n" + + " c1=@NestedClass[com.taobao.arthas.core.view.ObjectViewTest$NestedClass@ffffffff],\n" + + " c2=@NestedClass[com.taobao.arthas.core.view.ObjectViewTest$NestedClass@ffffffff],\n" + + " ],\n" + + " c2=@NestedClass[\n" + + " code=@Integer[2],\n" + + " c1=@NestedClass[com.taobao.arthas.core.view.ObjectViewTest$NestedClass@ffffffff],\n" + + " c2=@NestedClass[com.taobao.arthas.core.view.ObjectViewTest$NestedClass@ffffffff],\n" + + " ],\n" + + " ],\n" + + "]"; + Assert.assertEquals(expected, replaceHashCode(objectView.draw())); + } + + @Test + public void testObjectTooLarge() { + ObjectView objectView = new ObjectView(new NestedClass(100), 3, 100); + String expected = "@NestedClass[\n" + + " code=@Integer[100],\n" + + " c1=@NestedClass[\n" + + " code=@Integer[1],\n" + + " c1=...\n" + + "... Object size exceeds size limit:100"; + Assert.assertEquals(expected, objectView.draw()); + } + + private String replaceHashCode(String input) { + return input.replaceAll("@[0-9a-f]+", "@ffffffff"); + } + + + private static class NestedClass { + + private int code; + + private static NestedClass c1 = get(1); + private static NestedClass c2 = get(2); + + public NestedClass(int code) { + this.code = code; + } + + private static NestedClass get(int code) { + return new NestedClass(code); + } + } +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..766375dd --- /dev/null +++ b/pom.xml @@ -0,0 +1,100 @@ + + + 4.0.0 + + com.taobao.arthas + arthas-all + 3.0.0-SNAPSHOT + pom + + arthas-all + + + spy + core + agent + client + testcase + + + + UTF-8 + + + + + + org.ow2.asm + asm + 6.0 + + + org.ow2.asm + asm-commons + 6.0 + + + com.alibaba.middleware + termd-core + 1.1.4 + + + com.alibaba.middleware + cli + 1.0.0 + + + org.slf4j + slf4j-api + 1.7.5 + + + ch.qos.logback + logback-classic + 1.1.3 + + + ch.qos.logback + logback-core + 1.1.3 + + + com.taobao.middleware + logger.api + 0.2.4 + + + com.alibaba + fastjson + 1.2.33 + + + com.taobao.text + text.ui + 0.0.1 + + + com.fifesoft + rsyntaxtextarea + 2.5.8 + + + ognl + ognl + 3.1.12 + + + junit + junit + 4.11 + test + + + org.benf + cfr + 0.122 + + + + diff --git a/spy/pom.xml b/spy/pom.xml new file mode 100644 index 00000000..260c6ff4 --- /dev/null +++ b/spy/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + com.taobao.arthas + arthas-all + 3.0.0-SNAPSHOT + + arthas-spy + arthas-spy + + + arthas-spy + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + UTF-8 + true + + + + + + diff --git a/spy/src/main/java/java/arthas/Spy.java b/spy/src/main/java/java/arthas/Spy.java new file mode 100644 index 00000000..94f58266 --- /dev/null +++ b/spy/src/main/java/java/arthas/Spy.java @@ -0,0 +1,96 @@ +package java.arthas; + +import java.lang.reflect.Method; + +/** + * 间谍类
+ * 藏匿在各个ClassLoader中 + * Created by vlinux on 15/8/23. + */ +public class Spy { + + + // -- 各种Advice的钩子引用 -- + public static volatile Method ON_BEFORE_METHOD; + public static volatile Method ON_RETURN_METHOD; + public static volatile Method ON_THROWS_METHOD; + public static volatile Method BEFORE_INVOKING_METHOD; + public static volatile Method AFTER_INVOKING_METHOD; + public static volatile Method THROW_INVOKING_METHOD; + + /** + * arthas's classloader 引用 + */ + public static volatile ClassLoader CLASSLOADER; + + /** + * 代理重设方法 + */ + public static volatile Method AGENT_RESET_METHOD; + + /* + * 用于普通的间谍初始化 + */ + public static void init( + ClassLoader classLoader, + Method onBeforeMethod, + Method onReturnMethod, + Method onThrowsMethod, + Method beforeInvokingMethod, + Method afterInvokingMethod, + Method throwInvokingMethod) { + CLASSLOADER = classLoader; + ON_BEFORE_METHOD = onBeforeMethod; + ON_RETURN_METHOD = onReturnMethod; + ON_THROWS_METHOD = onThrowsMethod; + BEFORE_INVOKING_METHOD = beforeInvokingMethod; + AFTER_INVOKING_METHOD = afterInvokingMethod; + THROW_INVOKING_METHOD = throwInvokingMethod; + } + + /* + * 用于启动线程初始化 + */ + public static void initForAgentLauncher( + ClassLoader classLoader, + Method onBeforeMethod, + Method onReturnMethod, + Method onThrowsMethod, + Method beforeInvokingMethod, + Method afterInvokingMethod, + Method throwInvokingMethod, + Method agentResetMethod) { + CLASSLOADER = classLoader; + ON_BEFORE_METHOD = onBeforeMethod; + ON_RETURN_METHOD = onReturnMethod; + ON_THROWS_METHOD = onThrowsMethod; + BEFORE_INVOKING_METHOD = beforeInvokingMethod; + AFTER_INVOKING_METHOD = afterInvokingMethod; + THROW_INVOKING_METHOD = throwInvokingMethod; + AGENT_RESET_METHOD = agentResetMethod; + } + + /** + * Clean up the reference to com.taobao.arthas.agent.AgentLauncher$1 + * to avoid classloader leak. + */ + public static void destroy() { + CLASSLOADER = null; + ON_BEFORE_METHOD = null; + ON_RETURN_METHOD = null; + ON_THROWS_METHOD = null; + BEFORE_INVOKING_METHOD = null; + AFTER_INVOKING_METHOD = null; + THROW_INVOKING_METHOD = null; + // clear the reference to ArthasClassLoader in AgentLauncher + if (AGENT_RESET_METHOD != null) { + try { + AGENT_RESET_METHOD.invoke(null); + } catch (Exception e) { + e.printStackTrace(); + } + } + AGENT_RESET_METHOD = null; + } + +} diff --git a/testcase/pom.xml b/testcase/pom.xml new file mode 100644 index 00000000..a0d59df1 --- /dev/null +++ b/testcase/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + com.taobao.arthas + arthas-all + 3.0.0-SNAPSHOT + + arthas-testcase + arthas-testcase + + + + junit + junit + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + UTF-8 + + + + + diff --git a/testcase/src/main/java/com/alibaba/arthas/Pojo.java b/testcase/src/main/java/com/alibaba/arthas/Pojo.java new file mode 100644 index 00000000..fc1fddab --- /dev/null +++ b/testcase/src/main/java/com/alibaba/arthas/Pojo.java @@ -0,0 +1,31 @@ +package com.alibaba.arthas; + +public class Pojo { + String name; + int age; + String hobby; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getHobby() { + return hobby; + } + + public void setHobby(String hobby) { + this.hobby = hobby; + } +} diff --git a/testcase/src/main/java/com/alibaba/arthas/Test.java b/testcase/src/main/java/com/alibaba/arthas/Test.java new file mode 100644 index 00000000..25e3c202 --- /dev/null +++ b/testcase/src/main/java/com/alibaba/arthas/Test.java @@ -0,0 +1,57 @@ +package com.alibaba.arthas; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +/** + * @author diecui1202 on 2017/9/13. + */ +public class Test { + + public static final Map m = new HashMap(); + public static final Map n = new HashMap(); + + public static final Map p = null; + + static { + m.put("a", "aaa"); + m.put("b", "bbb"); + + n.put(Type.RUN, "aaa"); + n.put(Type.STOP, "bbb"); + } + + public static void main(String[] args) throws InterruptedException { + List list = new ArrayList(); + + for (int i = 0; i < 40; i ++) { + Pojo pojo = new Pojo(); + pojo.setName("name " + i); + pojo.setAge(i + 2); + + list.add(pojo); + } + + System.out.println(p); + + while (true) { + int random = new Random().nextInt(40); + String name = list.get(random).getName(); + list.get(random).setName(null); + test(list); + list.get(random).setName(name); + Thread.sleep(1000l); + } + } + + public static void test(List list) { + // do nothing + } + + public static void invoke(String a) { + System.out.println(a); + } +} diff --git a/testcase/src/main/java/com/alibaba/arthas/Type.java b/testcase/src/main/java/com/alibaba/arthas/Type.java new file mode 100644 index 00000000..21476b50 --- /dev/null +++ b/testcase/src/main/java/com/alibaba/arthas/Type.java @@ -0,0 +1,5 @@ +package com.alibaba.arthas; + +public enum Type { + RUN, STOP; +}